Files
binance-alpha-farm-agent/agent.user.js

1398 lines
62 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==UserScript==
// @name Binance Alpha Farm Agent
// @namespace http://baf.thuanle.me
// @version 2025.08.01
// @author TL
// @description Automated trading agent for Binance Alpha Farm
// @match https://www.binance.com/*
// @match https://accounts.binance.com/*
// @run-at document-idle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_notification
// @grant GM_xmlhttpRequest
// @grant GM_log
// @connect baf.thuanle.me
// @connect localhost
// @downloadURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js
// @updateURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js
// ==/UserScript==
if (window.top !== window) {
GM_log(`[TL] ❌ Skipping in iframe ${window.location.href}`);
return;
}
GM_log('[TL] 🏁 Welcome to Binance Alpha Farm Agent.');
(async () => {
'use strict';
const uuid = () => ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
const SESSION_STORAGE_KEYS = {
SESSION_ID: 'aRah9OhHeijee6sho3baequu9phoovah',
SESSION_BINDED: 'Oos2uoth2thae8Ir8iakahj1OohaMahb',
SESSION_TOKEN: 'ThiegiecohViuZ1Iecio7gahphiechub',
SESSION_INITIALIZED: 'iR4choo2Ieg2ew0ogheeyaer8Aisheet',
BOT_STATUS: 'wiethie3boGhoh3iegh3ohnezei2tauj',
CURRENT_TASK: 'Reebo1eitahh2aotumai5jae1neetoh3',
CURRENT_TASK_DATA: 'cheishailoh5keePoo6oe2Quie1gaxah',
CURRENT_STEP: 'eDusaidu2hooweiMoonahng3fua7aoso',
TIMER_START_TIME: 'einguh3eshuiqua7Ux7thaNgai5Axahy',
TIMER_IS_RUNNING: 'iengoose3vie2aikePhooCaPheeceizu',
TIMER_ACCUMULATED: 'aeMaiwae3PohB5Soshaech8eegh5quoo',
}
if (!sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_ID)) {
sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_ID, uuid());
}
// ====== CONFIGURATION ======
const CONFIG = {
HEARTBEAT_INTERVAL: 10_000, // Send heartbeat every 10 seconds
DASHBOARD_UPDATE_INTERVAL: 500, // Update dashboard overlay every 0.5 seconds
TASK_INTERVAL: 10_000, // Check for new task every 10 seconds
REQUEST_TIMEOUT: 10_000, // Request timeout in milliseconds (10 seconds)
DEBUG_MILESTONE: true, // Show milestone debug logs
SERVERS: {
local: { label: '🏠 Local', url: 'http://localhost:3000' },
prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' },
},
};
const AppSession = {
SESSION_ID: sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_ID),
getSessionBinded: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_BINDED) === 'true',
setSessionBinded: binded => sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_BINDED, binded ? 'true' : 'false'),
getSessionToken: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_TOKEN),
setSessionToken: token => sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_TOKEN, token),
getSessionStatus: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.BOT_STATUS),
setSessionStatus: status => sessionStorage.setItem(SESSION_STORAGE_KEYS.BOT_STATUS, status),
getSessionInitialized: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.SESSION_INITIALIZED) === 'true',
setSessionInitialized: initialized => sessionStorage.setItem(SESSION_STORAGE_KEYS.SESSION_INITIALIZED, initialized ? 'true' : 'false'),
// Task state - Trạng thái task hiện tại (lưu trong sessionStorage)
getCurrentTask: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.CURRENT_TASK),
setCurrentTask: task => sessionStorage.setItem(SESSION_STORAGE_KEYS.CURRENT_TASK, task),
getCurrentTaskData: () => {
const data = sessionStorage.getItem(SESSION_STORAGE_KEYS.CURRENT_TASK_DATA);
return data ? JSON.parse(data) : null;
},
setCurrentTaskData: data => sessionStorage.setItem(SESSION_STORAGE_KEYS.CURRENT_TASK_DATA, JSON.stringify(data)),
getCurrentStep: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.CURRENT_STEP),
setCurrentStep: step => sessionStorage.setItem(SESSION_STORAGE_KEYS.CURRENT_STEP, step),
initialize: async () => {
try {
const isInitialized = AppSession.getSessionInitialized();
if (!isInitialized) {
AppSession.setSessionStatus(AppEnums.BOT_STATUS.INITIALIZING);
AppSession.setCurrentTask(BAF_TASKS.NO_TASK);
AppSession.setCurrentStep(BAF_TASKS.NO_STEP);
AppSession.setSessionInitialized(true);
} else {
}
} catch (error) {
TL.error('APP-SESSION', '1.2.4. Error during session initialization:', error);
throw error;
}
},
}
// ====== APP ENUMS ======
const AppEnums = {
BOT_STATUS: {
INITIALIZING: 'initializing',
STOP: 'stop',
PAUSE: 'pause',
WAITING_FOR_TASK: 'waiting_for_task',
PERFORMING_TASK: 'performing_task',
ERROR: 'error',
},
BOT_STARTUP_MODE: {
AUTO: 'auto',
MANUAL: 'manual',
}
};
// ====== STATE MANAGEMENT ======
/**
* AppStateClass - Quản lý state của ứng dụng
* Lưu trữ runtime state (không persistent), persistent data được lưu trong AppSettings
*/
class AppStateClass {
constructor() {
this.data = {
// Server configuration - Thông tin server hiện tại
server_last_seen: null, // Thời gian cuối cùng ping server thành công
server_latency: null, // Thời gian ping server (ms)
// Page state - Trạng thái trang hiện tại
current_page: null, // Trang hiện tại (detected từ URL)
is_logged_in: false, // Trạng thái đăng nhập Binance
// Session flags - Flags cho session hiện tại (reset khi reload)
menuCreated: false, // Menu đã được tạo
popupInitialized: false, // Popup đã được khởi tạo
};
this.observers = new Map(); // Observer pattern cho state changes
this.initialized = false; // Flag khởi tạo
}
/**
* Khởi tạo state từ AppSettings và detect trang hiện tại
* Chỉ chạy một lần khi app khởi động
*/
async initialize() {
if (this.initialized) return;
this.initialized = true;
// Detect trang hiện tại (không block)
const currentPage = BINANCE.page.detectPage();
// Update state (không bao gồm login status)
await this.update({
current_page: currentPage,
});
TL.debug('APP-STATE', 'AppState initialized');
}
/**
* Lấy copy của data hiện tại
* @returns {Object} Copy của state data
*/
getData() {
return { ...this.data };
}
// ====== GETTER METHODS ======
getServer() { return CONFIG.SERVERS[AppSettings.getServerType()]; }
getServerLastSeen() { return this.data.server_last_seen; }
getServerLatency() { return this.data.server_latency; }
/**
* Kiểm tra xem server có kết nối không
* @returns {boolean} true nếu server kết nối trong 1 phút, false nếu không
*/
getServerConnected() {
return this.data.server_last_seen && Date.now() - this.data.server_last_seen < 60000;
}
getCurrentPage() { return this.data.current_page; }
getIsLoggedIn() { return this.data.is_logged_in; }
getMenuCreated() { return this.data.menuCreated; }
getPopupInitialized() { return this.data.popupInitialized; }
/**
* Cập nhật state với partial update
* @param {Object} updates - Object chứa cặp key-value cần cập nhật vào state
* Ví dụ: { current_task: task, is_loading: true }
*/
async update(updates) {
const oldData = { ...this.data };
Object.assign(this.data, updates);
await this.notifyObservers(oldData, this.data);
// TL.debug('STATE', 'Updated', updates);
}
/**
* Đăng ký observer cho state change
* @param {string} key - Key của state cần observe
* @param {Function} callback - Callback function khi state thay đổi
*/
subscribe(key, callback) {
if (!this.observers.has(key)) {
this.observers.set(key, []);
}
this.observers.get(key).push(callback);
}
/**
* Hủy đăng ký observer
* @param {string} key - Key của state
* @param {Function} callback - Callback function cần hủy
*/
unsubscribe(key, callback) {
const callbacks = this.observers.get(key);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
/**
* Thông báo tất cả observers khi state thay đổi
* @param {Object} oldData - State cũ
* @param {Object} newData - State mới
*/
async notifyObservers(oldData, newData) {
for (const [key, callbacks] of this.observers) {
const oldValue = oldData[key];
const newValue = newData[key];
if (oldValue !== newValue) {
for (const callback of callbacks) {
try {
await callback(oldValue, newValue, newData);
} catch (error) {
TL.error('STATE', `Observer error for ${key}:`, error);
}
}
}
}
}
}
const AppState = new AppStateClass();
// ====== APP SETTINGS ======
/**
* AppSettings - Quản lý persistent settings của ứng dụng
* Lưu trữ cài đặt người dùng trong GM storage (vĩnh viễn)
*/
const AppSettings = {
key_agent_token: 'baf-agent-token',
key_server_type: 'baf-server-type',
key_bot_startup_mode: 'baf-bot-startup-mode',
key_popup_position: 'baf-popup-position',
key_popup_visible: 'baf-popup-visible',
key_debug: 'baf-debug',
key_safety_guard: 'baf-safety-guard',
getAgentToken: () => GM_getValue(AppSettings.key_agent_token, ''),
setAgentToken: token => GM_setValue(AppSettings.key_agent_token, String(token || '').trim().replace(/^Bearer\s+/i, '')),
// Server configuration
getServerType: () => GM_getValue(AppSettings.key_server_type, 'prod'),
setServerType: (type) => {
GM_setValue(AppSettings.key_server_type, type);
AppState.update({ server_last_seen: null });
},
getBotStartupMode: () => GM_getValue(AppSettings.key_bot_startup_mode, AppEnums.BOT_STARTUP_MODE.MANUAL),
setBotStartupMode: (mode) => GM_setValue(AppSettings.key_bot_startup_mode, mode),
// Popup configuration
getPopupPosition: () => GM_getValue(AppSettings.key_popup_position, 4),
setPopupPosition: (position) => GM_setValue(AppSettings.key_popup_position, position),
getPopupVisible: () => GM_getValue(AppSettings.key_popup_visible, true),
setPopupVisible: (visible) => GM_setValue(AppSettings.key_popup_visible, visible),
// Debug mode
getDebug: () => GM_getValue(AppSettings.key_debug, true),
setDebug: (debug) => GM_setValue(AppSettings.key_debug, debug),
// Safety guard
getSafetyGuard: () => GM_getValue(AppSettings.key_safety_guard, true),
setSafetyGuard: (safetyGuard) => {
GM_setValue(AppSettings.key_safety_guard, safetyGuard);
GM_log(`[TL] Safety guard set to: ${safetyGuard}`);
},
};
// ====== COMMON UTILITIES ======
/**
* TL - Common utilities cho logging, notifications, và DOM helpers
*/
const TL = {
debug: (tag, msg, ...args) => AppSettings.getDebug() && GM_log(`[TL] [DEBUG] [${new Date().toLocaleString()}] [${tag}] \n${msg}`, ...args),
milestone: (tag, msg, ...args) => CONFIG.DEBUG_MILESTONE && GM_log(`[TL] [MILESTONE] [${new Date().toLocaleString()}] [${tag}] 🎯\n${msg}`, ...args),
log: (tag, msg, ...args) => GM_log(`[TL] [LOG] [${new Date().toLocaleString()}] [${tag}] \n${msg}`, ...args),
warn: (tag, msg, ...args) => GM_log(`[TL] [WARN] [${new Date().toLocaleString()}] [${tag}] ⚠️\n${msg}`, ...args),
error: (tag, msg, ...args) => GM_log(`[TL] [ERROR] [${new Date().toLocaleString()}] [${tag}] ❌\n${msg}`, ...args),
noti: (title, text, timeout = 2500) => {
if (typeof GM_notification === 'function') {
GM_notification({ title, text, timeout });
return true;
}
alert(`${title}\n${text}`);
return false;
},
delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
// DOM helpers
dom: {
isVisible: (ele) => {
if (!ele) return false;
const cs = getComputedStyle(ele);
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
return (ele.offsetWidth + ele.offsetHeight) > 0;
},
isDisabled: (ele) => {
return ele?.disabled === true || ele?.getAttribute('aria-disabled') === 'true';
},
isInViewport: (ele) => {
if (!ele) return false;
const rect = ele.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
},
click: (ele) => {
if (ele && typeof ele.click === 'function') ele.click();
},
scrollToView: (ele, behavior = 'smooth') => {
ele?.scrollIntoView?.({ behavior, block: 'center' });
},
setHover: (ele, eventType, styles) => {
if (!ele) return;
const applyStyles = () => {
if (styles.scale) ele.style.transform = `scale(${styles.scale})`;
if (styles.transform) ele.style.transform = styles.transform;
if (styles.background) ele.style.background = styles.background;
if (styles.color) ele.style.color = styles.color;
if (styles.border) ele.style.border = styles.border;
};
ele.addEventListener(eventType, applyStyles);
},
},
// Browser helpers
browser: {
/**
* Navigate to URL với option forceReload
* @param {string} url - URL cần navigate
*/
navigateTo: (url) => {
window.location.href = url;
}
},
// Network helpers
net: {
gmRequest(url, init = {}) {
const headersToObject = (h) => {
const o = {};
(h || new Headers()).forEach((v, k) => { o[k] = v; });
return o;
};
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
url,
method: (init.method || 'GET').toUpperCase(),
headers: headersToObject(init.headers),
data: init.body,
timeout: CONFIG.REQUEST_TIMEOUT,
onload: (resp) => {
const text = resp.responseText || '';
let data = text;
const isJSON = /content-type:\s*application\/json/i.test(resp.responseHeaders || '');
if (isJSON) { try { data = JSON.parse(text); } catch { } }
TL.debug(`net`, `${init.method} ${resp.status} ${url}\n${init.body}`, data);
resolve({
status: resp.status,
ok: resp.status >= 200 && resp.status < 300,
data,
rawText: text,
headers: null
});
},
onerror: (error) => {
TL.error('net', `Request failed: ${url}`, error);
resolve({
status: 0,
ok: false,
data: null,
rawText: '',
headers: null,
error: 'Network error'
});
},
ontimeout: () => {
TL.error('net', `Request timeout after ${CONFIG.REQUEST_TIMEOUT}ms: ${url}`);
resolve({
status: 0,
ok: false,
data: null,
rawText: '',
headers: null,
error: 'Request timeout'
});
},
});
});
},
}
};
// ====== BAF API MODULE ======
const BAF_TASKS = {
NO_TASK: 'no_task',
LOGIN: 'login',
WALLET_SPOT: 'wallet_spot',
WALLET_FUNDING: 'wallet_funding',
WALLET_EARN: 'wallet_earn',
ALPHA_SWAP: 'alpha_swap',
ALPHA_ORDER_HISTORY: 'alpha_order_history',
NO_STEP: 'no_step',
};
const BAF = {
_getHost: () => AppState.getServer()?.url,
_request: async (method, path, { params, body, headers, token } = {}) => {
const base = BAF._getHost();
const url = new URL(path, base);
if (params) for (const [k, v] of Object.entries(params)) url.searchParams.append(k, v);
const h = new Headers(headers || {});
if (token && !h.has('Authorization')) h.set('Authorization', `Bearer ${token}`);
const init = { method, headers: h };
if (body !== undefined && body !== null) {
if (!(body instanceof FormData) && !h.has('Content-Type')) h.set('Content-Type', 'application/json');
init.body = (h.get('Content-Type') || '').includes('application/json') && typeof body !== 'string'
? JSON.stringify(body)
: body;
}
return TL.net.gmRequest(url.toString(), init);
},
_agentGet: (path, params, init = {}) => BAF._request('GET', path, { params, headers: init.headers, token: AppSettings.getAgentToken() }),
_agentPost: (path, body, init = {}) => BAF._request('POST', path, { body, headers: init.headers, token: AppSettings.getAgentToken() }),
_sessionGet: (path, params, init = {}) => BAF._request('GET', path, { params, headers: init.headers, token: AppSession.getSessionToken() }),
_sessionPost: (path, body, init = {}) => BAF._request('POST', path, { body, headers: init.headers, token: AppSession.getSessionToken() }),
register: (data) => BAF._agentPost('/agent/register', data),
ping: (data) => BAF._sessionPost('/session/ping', data),
getTask: (data) => BAF._sessionPost('/session/task', data),
};
// ====== BINANCE MODULE ======
// Binance Pages constants
const BINANCE_PAGES = {
LOGIN: 'login',
ALPHA_SWAP: 'alpha-swap',
ALPHA_ORDER_HISTORY: 'alpha-order-history',
WALLET_EARN: 'wallet-earn',
WALLET_FUNDING: 'wallet-funding',
WALLET_SPOT: 'wallet-spot',
IGNORE: 'ignore',
UNKNOWN: 'unknown'
};
// Page patterns for detection
const BINANCE_PAGE_PATTERNS = [
{
host: 'accounts.binance.com',
patterns: [
{ includes: '/login', page: BINANCE_PAGES.LOGIN }
]
},
{
host: 'www.binance.com',
patterns: [
{ includes: '/alpha/bsc/', page: BINANCE_PAGES.ALPHA_SWAP },
{ includes: '/my/orders/alpha/orderhistory', page: BINANCE_PAGES.ALPHA_ORDER_HISTORY },
{ includes: '/my/wallet/account/earn', page: BINANCE_PAGES.WALLET_EARN },
{ includes: '/my/wallet/funding', page: BINANCE_PAGES.WALLET_FUNDING },
{ includes: '/my/wallet/account/main', page: BINANCE_PAGES.WALLET_SPOT }
]
}
];
const BINANCE = {
page: {
detectPage: () => {
const { hostname, pathname } = window.location;
if (window.top !== window) {
TL.debug('BINANCE-PAGE-DETECT', 'Call from iframe ❌');
return BINANCE_PAGES.IGNORE;
}
const hostConfig = BINANCE_PAGE_PATTERNS.find(cfg => cfg.host === hostname);
if (!hostConfig) {
return BINANCE_PAGES.UNKNOWN;
}
// Check patterns in order (most specific first)
for (const pattern of hostConfig.patterns) {
if (pathname.includes(pattern.includes)) {
TL.debug('BINANCE-PAGE-DETECT', `Matched pattern: ${pattern.includes} -> ${pattern.page}`);
return pattern.page;
}
}
TL.debug('BINANCE-PAGE-DETECT', `No pattern matched: ${hostname}${pathname}`);
return BINANCE_PAGES.UNKNOWN;
},
isOnLoginPage: () => AppState.getCurrentPage() === BINANCE_PAGES.LOGIN,
isOnAlphaSwapPage: (contractAddress) => {
const page = AppState.getCurrentPage();
return page === BINANCE_PAGES.ALPHA_SWAP && (!contractAddress || window.location.pathname.includes(contractAddress));
},
isOnAlphaOrderHistoryPage: () => AppState.getCurrentPage() === BINANCE_PAGES.ALPHA_ORDER_HISTORY,
isOnWalletEarnPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_EARN,
isOnWalletFundingPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_FUNDING,
isOnWalletSpotPage: () => AppState.getCurrentPage() === BINANCE_PAGES.WALLET_SPOT,
// Navigation URL generators - chỉ trả về address, không thực hiện navigate
getLoginUrl: () => 'https://www.binance.com/en/login',
getAlphaSwapUrl: (contractAddress) => {
return contractAddress
? `https://www.binance.com/en/alpha/bsc/${contractAddress}`
: `https://www.binance.com/en/alpha`;
},
getAlphaOrderHistoryUrl: () => 'https://www.binance.com/en/my/orders/alpha/orderhistory',
getWalletEarnUrl: () => 'https://www.binance.com/en/my/wallet/account/earn',
getWalletFundingUrl: () => 'https://www.binance.com/en/my/wallet/funding',
getWalletSpotUrl: () => 'https://www.binance.com/en/my/wallet/account/main'
},
auth: {
detectLoginState: () => {
if (!AppSettings.getSafetyGuard()) return null;
if (window.top !== window) {
TL.debug('BINANCE-LOGIN', 'In iframe - cannot determine login state');
return null;
}
if (BINANCE.page.isOnLoginPage()) {
return false;
}
// Method 1: Check for login/register buttons (indicates NOT logged in)
const loginBtn = document.querySelector('#toLoginPage, [data-testid="login-button"], .login-btn');
const regBtn = document.querySelector('#toRegisterPage, [data-testid="register-button"], .register-btn');
let msg = `loginBtn: ${TL.dom.isVisible?.(loginBtn)} regBtn: ${TL.dom.isVisible?.(regBtn)}\n`;
// If login/register buttons are visible, definitely not logged in
if (TL.dom.isVisible?.(loginBtn) || TL.dom.isVisible?.(regBtn)) {
TL.debug('BINANCE-LOGIN', msg + 'Login/Register buttons visible -> NOT logged in');
return false;
}
// Method 2: Check for user account elements (indicates logged in)
const dashboardLink = document.querySelector('a[href*="/my/dashboard"]');
const accountIcon = document.querySelector('.header-account-icon');
const walletBtn = document.querySelector('#ba-wallet');
const isDashboardVisible = dashboardLink && TL.dom.isVisible?.(dashboardLink);
const isAccountIconVisible = !!accountIcon;
const isWalletVisible = walletBtn && TL.dom.isVisible?.(walletBtn);
msg += `Visible: dashboard: ${isDashboardVisible ? '✓' : '✗'}, accountIcon: ${isAccountIconVisible ? '✓' : '✗'}, walletBtn: ${isWalletVisible ? '✓' : '✗'}\n`;
// If we see user account elements, likely logged in
if (isDashboardVisible && isAccountIconVisible && isWalletVisible) {
TL.debug('BINANCE-LOGIN', msg + 'User account elements found -> LIKELY logged in');
return true;
}
TL.debug('BINANCE-LOGIN', msg + 'Cannot determine login state - returning null');
return null;
},
isLoggedIn: async (timeoutMs = 30000, pollMs = 500) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const state = BINANCE.auth.detectLoginState();
if (state !== null) {
TL.debug(`BINANCE-LOGIN`, `isLoggedIn: ${state ? 'true' : 'false'}`);
return state;
}
TL.debug(`BINANCE-LOGIN`, `isLoggedIn: not found. Sleeping...`);
await TL.delay(pollMs);
}
const fallback = BINANCE.auth.detectLoginState() ?? false;
TL.debug(`BINANCE-LOGIN`, `isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`);
return fallback;
}
}
};
// ====== APP UI ======
const AppUi = {
// ====== DASHBOARD OVERLAY ======
dashboardOverlay: {
element: null,
// Get position CSS based on position number
getPositionCSS: (position) => {
switch (position) {
case 1: return { top: '10px', left: '10px', bottom: 'auto', right: 'auto' }; // top-left
case 2: return { top: '10px', right: '10px', bottom: 'auto', left: 'auto' }; // top-right
case 3: return { bottom: '10px', left: '10px', top: 'auto', right: 'auto' }; // bottom-left
case 4: return { bottom: '10px', right: '10px', top: 'auto', left: 'auto' }; // bottom-right
default: return { bottom: '10px', right: '10px', top: 'auto', left: 'auto' };
}
},
// Create dashboard overlay element
create: () => {
if (AppUi.dashboardOverlay.element) {
document.body.removeChild(AppUi.dashboardOverlay.element);
}
const overlay = document.createElement('div');
overlay.id = 'baf-status-overlay';
overlay.style.cssText = `
position: fixed;
z-index: 999999;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 12px;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
line-height: 1.4;
min-width: 200px;
max-width: 300px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
user-select: none;
`;
// Add close button (X)
const closeBtn = document.createElement('div');
closeBtn.style.cssText = `
position: absolute;
top: -8px;
left: -8px;
width: 20px;
height: 20px;
background: #dc3545;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 12px;
color: white;
border: 2px solid rgba(255, 255, 255, 0.2);
transition: all 0.2s ease;
font-weight: bold;
`;
closeBtn.innerHTML = '×';
closeBtn.title = 'Close popup';
closeBtn.addEventListener('click', () => {
AppUi.dashboardOverlay.hide();
AppSettings.setPopupVisible(false);
});
TL.dom.setHover(closeBtn, 'mouseenter', { scale: 1.5, background: '#c82333' });
TL.dom.setHover(closeBtn, 'mouseleave', { scale: 1, background: '#dc3545' });
overlay.appendChild(closeBtn);
// Add position toggle button
const positionBtn = document.createElement('div');
positionBtn.style.cssText = `
position: absolute;
top: -8px;
right: -8px;
width: 20px;
height: 20px;
background: #007bff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 10px;
color: white;
border: 2px solid rgba(255, 255, 255, 0.2);
transition: all 0.2s ease;
`;
positionBtn.innerHTML = '⇄';
positionBtn.title = 'Change position';
positionBtn.addEventListener('click', () => {
const newPosition = (AppSettings.getPopupPosition() % 4) + 1;
AppSettings.setPopupPosition(newPosition);
AppUi.dashboardOverlay.updatePosition();
});
TL.dom.setHover(positionBtn, 'mouseenter', { scale: 1.5, background: '#0056b3' });
TL.dom.setHover(positionBtn, 'mouseleave', { scale: 1, background: '#007bff' });
overlay.appendChild(positionBtn);
AppUi.dashboardOverlay.element = overlay;
document.body.appendChild(overlay);
return overlay;
},
// Update overlay position
updatePosition: () => {
if (!AppUi.dashboardOverlay.element) return;
const positionCSS = AppUi.dashboardOverlay.getPositionCSS(AppSettings.getPopupPosition());
Object.assign(AppUi.dashboardOverlay.element.style, positionCSS);
},
// Update overlay content
updateContent: () => {
try {
if (!AppUi.dashboardOverlay.element) {
AppUi.dashboardOverlay.create();
}
// Get status display
let statusDisplay = '';
const currentStatus = AppSession.getSessionStatus();
switch (currentStatus) {
case AppEnums.BOT_STATUS.INITIALIZING:
statusDisplay = '🔄 Initializing';
break;
case AppEnums.BOT_STATUS.STOP:
statusDisplay = '⏹️ Stop';
break;
case AppEnums.BOT_STATUS.PAUSE:
statusDisplay = '⏸️ Pause';
break;
case AppEnums.BOT_STATUS.WAITING_FOR_TASK:
statusDisplay = '⏳ Waiting for Task';
break;
case AppEnums.BOT_STATUS.PERFORMING_TASK:
statusDisplay = '▶️ Performing Task';
break;
case AppEnums.BOT_STATUS.ERROR:
statusDisplay = '❌ Error';
break;
default:
statusDisplay = '❓ Unknown';
break;
}
// Get server info
const serverLabel = AppState.getServer()?.label || 'Unknown';
const serverConnected = AppState.getServerConnected() ? `🟢 (${AppState.getServerLatency()}ms)` : '🔴';
// Get page info
const pageDisplay = AppState.getCurrentPage() || 'unknown';
// Get login status
const loginStatus = AppState.getIsLoggedIn() ? '✅' : '❌';
// Get SafetyGuard status
const safetyStatus = {
enabled: AppSettings.getSafetyGuard(),
message: AppSettings.getSafetyGuard() ? '✅ Operations Enabled' : '🚨 Operations Blocked'
};
// Build content
const content = `
<div style="margin-bottom: 8px;"><strong>BAF Agent Dashboard</strong></div>
<div style="margin-bottom: 4px;">
<span id="safety-toggle-btn-AithaiG2" style="color: ${safetyStatus.enabled ? '#28a745' : '#dc3545'}; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid ${safetyStatus.enabled ? '#28a745' : '#dc3545'}; background: ${safetyStatus.enabled ? 'rgba(40, 167, 69, 0.1)' : 'rgba(220, 53, 69, 0.1)'}; transition: all 0.2s ease;" title="Click to toggle safety status">🛟 Safety: ${safetyStatus.enabled ? '🛡️ Safe' : '🚨 Blocked'}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #6c757d; font-size: 11px;">🆔 Tab: ${AppSession.SESSION_ID.slice(-12)}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #007bff; padding: 2px 4px; background: rgba(0, 123, 255, 0.1);" title="Current bot status">🤖 Bot Status: ${statusDisplay}</span>
</div>
<div style="margin-bottom: 4px;">
<span id="startup-mode-toggle-btn-tha9ieCa" style="color: #ffc107; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid #ffc107; background: rgba(255, 193, 7, 0.1); transition: all 0.2s ease;" title="Click to toggle startup mode between auto and manual">🚀 Startup Mode: ${AppSettings.getBotStartupMode() === AppEnums.BOT_STARTUP_MODE.AUTO ? '🔄 Auto' : '✋ Manual'}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #28a745;">📋 Task: ${AppSession.getCurrentTask() || 'None'}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #17a2b8;">📍 Step: ${AppSession.getCurrentStep() || 'None'}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #fd7e14;">👤 Login: ${loginStatus}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: #6f42c1;">Page: ${pageDisplay}</span>
</div>
<div style="margin-bottom: 4px;">
<span id="server-toggle-btn-Youx4pho" style="color: #e83e8c; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid #e83e8c; background: rgba(232, 62, 140, 0.1); transition: all 0.2s ease;" title="Click to switch between local and prod servers">🌐 Server: ${serverLabel} ${serverConnected}</span>
</div>
<div style="margin-bottom: 4px;">
<span id="debug-toggle-btn-Tai5guto" style="color: ${AppSettings.getDebug() ? '#17a2b8' : '#6c757d'}; cursor: pointer; padding: 2px 4px; border-radius: 3px; border: 1px solid ${AppSettings.getDebug() ? '#17a2b8' : '#6c757d'}; background: ${AppSettings.getDebug() ? 'rgba(23, 162, 184, 0.1)' : 'rgba(108, 117, 125, 0.1)'}; transition: all 0.2s ease;" title="Click to toggle debug mode">🐛 Debug: ${AppSettings.getDebug() ? '🔈 ON' : '🔇 OFF'}</span>
</div>
<div style="margin-bottom: 4px;">
<span style="color: ${CONFIG.DEBUG_MILESTONE ? '#28a745' : '#6c757d'}; padding: 2px 4px; border-radius: 3px; border: 1px solid ${CONFIG.DEBUG_MILESTONE ? '#28a745' : '#6c757d'}; background: ${CONFIG.DEBUG_MILESTONE ? 'rgba(40, 167, 69, 0.1)' : 'rgba(108, 117, 125, 0.1)'};" title="Milestone debug mode (configured in CONFIG)">🎯 Milestone: ${CONFIG.DEBUG_MILESTONE ? '🔈 ON' : '🔇 OFF'}</span>
</div>
<div style="margin-top: 8px; border-top: 1px solid rgba(255, 255, 255, 0.2); padding-top: 8px;">
<div style="text-align: center; margin-bottom: 8px;">
<span id="timer-display-ohr1Youj" style="color: #20c997; font-family: 'Courier New', monospace; font-size: 16px; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 1px solid #20c997; background: rgba(32, 201, 151, 0.1);" title="Session timer">⏱️ ${AppServices.Timer.getDisplay()}</span>
</div>
<div style="display: flex; gap: 4px; justify-content: center;">
<span id="play-btn-uPha3Mah" style="color: #28a745; cursor: pointer; padding: 4px 8px; border-radius: 4px; border: 1px solid #28a745; background: ${currentStatus === AppEnums.BOT_STATUS.WAITING_FOR_TASK || currentStatus === AppEnums.BOT_STATUS.PERFORMING_TASK ? 'rgba(40, 167, 69, 0.3)' : 'rgba(40, 167, 69, 0.1)'}; transition: all 0.2s ease; font-size: 14px;" title="Play - Start waiting for tasks">▶️</span>
<span id="pause-btn-Gai2quie" style="color: #ffc107; cursor: pointer; padding: 4px 8px; border-radius: 4px; border: 1px solid #ffc107; background: ${currentStatus === AppEnums.BOT_STATUS.PAUSE ? 'rgba(255, 193, 7, 0.3)' : 'rgba(255, 193, 7, 0.1)'}; transition: all 0.2s ease; font-size: 14px;" title="Pause - Stop but keep current task">⏸️</span>
<span id="stop-btn-jooK6che" style="color: #dc3545; cursor: pointer; padding: 4px 8px; border-radius: 4px; border: 1px solid #dc3545; background: ${currentStatus === AppEnums.BOT_STATUS.STOP || currentStatus === AppEnums.BOT_STATUS.ERROR ? 'rgba(220, 53, 69, 0.3)' : 'rgba(220, 53, 69, 0.1)'}; transition: all 0.2s ease; font-size: 14px;" title="Stop - Clear task and stop">⏹️</span>
</div>
</div>
`;
// Update content (excluding position button)
if (!AppUi.dashboardOverlay.element) {
TL.error('DASHBOARD-OVERLAY', '8.1. Element is null!');
return;
}
const contentDiv = AppUi.dashboardOverlay.element.querySelector('.baf-content') || document.createElement('div');
contentDiv.className = 'baf-content';
contentDiv.innerHTML = content;
if (!AppUi.dashboardOverlay.element.querySelector('.baf-content')) {
AppUi.dashboardOverlay.element.appendChild(contentDiv);
}
// Add event listener for safety toggle button
const safetyToggleBtn = contentDiv.querySelector('#safety-toggle-btn-AithaiG2');
if (safetyToggleBtn) {
safetyToggleBtn.onclick = () => {
const currentStatus = {
enabled: AppSettings.getSafetyGuard(),
message: AppSettings.getSafetyGuard() ? '✅ Operations Enabled' : '🚨 Operations Blocked'
};
if (currentStatus.enabled) {
AppSettings.setSafetyGuard(false);
} else {
AppSettings.setSafetyGuard(true);
}
// Update the content to reflect the change
AppUi.dashboardOverlay.updateContent();
};
}
// Add event listener for debug toggle button
const debugToggleBtn = contentDiv.querySelector('#debug-toggle-btn-Tai5guto');
if (debugToggleBtn) {
debugToggleBtn.onclick = () => {
const newDebugState = !AppSettings.getDebug();
AppSettings.setDebug(newDebugState);
// Update the content to reflect the change
AppUi.dashboardOverlay.updateContent();
};
}
// Add event listeners for control buttons
const playBtn = contentDiv.querySelector('#play-btn-uPha3Mah');
if (playBtn) {
playBtn.onclick = () => {
AppServices.Status.play();
AppUi.dashboardOverlay.updateContent();
};
}
const pauseBtn = contentDiv.querySelector('#pause-btn-Gai2quie');
if (pauseBtn) {
pauseBtn.onclick = () => {
AppServices.Status.pause();
AppUi.dashboardOverlay.updateContent();
};
}
const stopBtn = contentDiv.querySelector('#stop-btn-jooK6che');
if (stopBtn) {
stopBtn.onclick = () => {
AppServices.Status.stop();
AppUi.dashboardOverlay.updateContent();
};
}
// Add hover effects for control buttons
if (playBtn) {
TL.dom.setHover(playBtn, 'mouseenter', { scale: 1.2, background: 'rgba(40, 167, 69, 0.2)' });
TL.dom.setHover(playBtn, 'mouseleave', { scale: 1, background: 'rgba(40, 167, 69, 0.1)' });
}
if (pauseBtn) {
TL.dom.setHover(pauseBtn, 'mouseenter', { scale: 1.2, background: 'rgba(255, 193, 7, 0.2)' });
TL.dom.setHover(pauseBtn, 'mouseleave', { scale: 1, background: 'rgba(255, 193, 7, 0.1)' });
}
if (stopBtn) {
TL.dom.setHover(stopBtn, 'mouseenter', { scale: 1.2, background: 'rgba(220, 53, 69, 0.2)' });
TL.dom.setHover(stopBtn, 'mouseleave', { scale: 1, background: 'rgba(220, 53, 69, 0.1)' });
}
// Add event listener for startup mode toggle button
const startupModeToggleBtn = contentDiv.querySelector('#startup-mode-toggle-btn-tha9ieCa');
if (startupModeToggleBtn) {
startupModeToggleBtn.onclick = () => {
const currentStartupMode = AppSettings.getBotStartupMode();
const newStartupMode = currentStartupMode === AppEnums.BOT_STARTUP_MODE.AUTO ? AppEnums.BOT_STARTUP_MODE.MANUAL : AppEnums.BOT_STARTUP_MODE.AUTO;
AppSettings.setBotStartupMode(newStartupMode);
// Update the content to reflect the change
AppUi.dashboardOverlay.updateContent();
};
}
// Add event listener for server toggle button
const serverToggleBtn = contentDiv.querySelector('#server-toggle-btn-Youx4pho');
if (serverToggleBtn) {
serverToggleBtn.onclick = async () => {
const currentServerType = AppSettings.getServerType();
const newServerType = currentServerType === 'local' ? 'prod' : 'local';
// Update server type in settings
AppSettings.setServerType(newServerType);
await AppState.update({ server_last_seen: null });
// Update the content to reflect the change
AppUi.dashboardOverlay.updateContent();
};
}
} catch (error) {
TL.error('DASHBOARD-OVERLAY', 'Error updating content:', error);
}
},
// Show dashboard overlay
show: () => {
if (!AppUi.dashboardOverlay.element) {
AppUi.dashboardOverlay.create();
}
AppUi.dashboardOverlay.updatePosition();
AppUi.dashboardOverlay.updateContent();
AppUi.dashboardOverlay.element.style.display = 'block';
},
// Hide dashboard overlay
hide: () => {
if (AppUi.dashboardOverlay.element) {
AppUi.dashboardOverlay.element.style.display = 'none';
}
},
// Toggle dashboard overlay
toggle: () => {
if (AppSettings.getPopupVisible()) {
AppUi.dashboardOverlay.hide();
AppSettings.setPopupVisible(false);
} else {
AppUi.dashboardOverlay.show();
AppSettings.setPopupVisible(true);
}
},
// Initialize dashboard overlay
init: () => {
try {
const popupVisible = AppSettings.getPopupVisible();
if (popupVisible) {
AppUi.dashboardOverlay.show();
} else {
}
} catch (error) {
TL.error('DASHBOARD-OVERLAY', '1.4.4. Error initializing dashboard overlay:', error);
throw error;
}
}
},
// ====== MENU SYSTEM ======
menu: {
// Create GM menu
create: async () => {
try {
if (AppState.getMenuCreated()) {
return;
}
AppState.update({ menuCreated: true });
GM_registerMenuCommand('🛟 Safety Guard: -> ✅ Enable Operations', () => {
AppSettings.setSafetyGuard(true);
});
GM_registerMenuCommand('🛟 Safety Guard: -> 🚨 Block Operations', () => {
AppSettings.setSafetyGuard(false);
});
GM_registerMenuCommand('🔑 Token', async () => {
try {
const curToken = await AppSettings.getToken();
const input = prompt('Bearer token:', curToken || '');
if (input !== null && input.trim() && input.trim() !== curToken) {
await AppSettings.setToken(input);
}
const s = AppState.getServer();
const res = await BAF.register({ session_id: AppSession.SESSION_ID });
const resStr =
`Server: ${s.label} (${s.url})\n` +
`Status: ${res.ok ? 'Connected ✅' : 'Failed ❌'} (${res.status})`;
TL.debug(`BAF`, resStr);
TL.noti('BAF Server', resStr);
} catch (e) {
const resStr = `ping error: ${e.message}`;
TL.error('BAF', resStr);
TL.noti('BAF Server', resStr);
}
});
// Popup toggle menu
GM_registerMenuCommand("👁️ Toggle Popup", () => {
AppUi.dashboardOverlay.toggle();
});
} catch (error) {
TL.error('MENU', '1.3.5. Failed to create menu:', error);
throw error;
}
}
},
};
// ====== APP TASKS ======
const AppTasks = {
login: async (step, data) => {
if (!AppServices.isReadyToRun()) {
TL.debug('APP-TASKS', 'Bot is not ready to run');
return;
}
TL.debug('APP-TASKS', `Login task ${step}`);
//set steps
switch (step) {
case "start":
AppSession.setSessionStatus(AppEnums.BOT_STATUS.PERFORMING_TASK);
AppSession.setCurrentStep("navigating-to-login-page");
TL.browser.navigateTo(BINANCE.page.getLoginUrl());
break;
}
},
};
// ====== APP SERVICE ======
const AppServices = {
initialize: async () => {
try {
await AppState.initialize();
await AppSession.initialize();
// Khởi tạo timer
AppServices.Timer.initialize();
await AppUi.menu.create();
AppUi.dashboardOverlay.init();
await AppServices.registerSession();
const status = AppSession.getSessionStatus();
if (status === AppEnums.BOT_STATUS.INITIALIZING) {
const startupMode = AppSettings.getBotStartupMode();
if (startupMode === AppEnums.BOT_STARTUP_MODE.AUTO) {
AppSession.setSessionStatus(AppEnums.BOT_STATUS.WAITING_FOR_TASK);
} else {
AppSession.setSessionStatus(AppEnums.BOT_STATUS.STOP);
}
}
AppServices.initInterval();
} catch (error) {
TL.error('APP-SERVICES', 'Error during initialization:', error);
throw error;
}
},
registerSession: async () => {
try {
const msg = await BAF.register({ session_id: AppSession.SESSION_ID });
if (msg.ok) {
AppSession.setSessionToken(msg.data.token);
AppSession.setSessionBinded(true);
} else {
AppSession.setSessionBinded(false);
TL.error('APP-SERVICES', `1.5.3. Failed to register session: ${msg.error}`);
}
} catch (error) {
TL.error('APP-SERVICES', '1.5.3. Error during session registration:', error);
AppSession.setSessionBinded(false);
throw error;
}
},
initInterval: () => {
try {
setInterval(() => AppServices.heartbeat(), CONFIG.HEARTBEAT_INTERVAL);
setInterval(() => AppUi.dashboardOverlay.updateContent(), CONFIG.DASHBOARD_UPDATE_INTERVAL);
// Timer update interval - cập nhật timer mỗi giây
setInterval(() => {
const timerDisplay = document.querySelector('#timer-display-ohr1Youj');
if (timerDisplay) {
timerDisplay.textContent = `⏱️ ${AppServices.Timer.getDisplay()}`;
}
}, 1000);
} catch (error) {
throw error;
}
},
isConnected: () => {
const serverLastSeen = AppState.getServerLastSeen();
return serverLastSeen && new Date() - serverLastSeen < 60000;
},
isReadyToRun: () => {
return AppSession.getSessionBinded() && AppSettings.getSafetyGuard() && AppSession.getSessionStatus() === AppEnums.BOT_STATUS.WAITING_FOR_TASK;
},
// ====== TIMER SYSTEM ======
Timer: {
// Timer state management - Quản lý trạng thái timer
getStartTime: () => {
const time = sessionStorage.getItem(SESSION_STORAGE_KEYS.TIMER_START_TIME);
return time ? parseInt(time) : null;
},
setStartTime: time => {
if (time === null || time === undefined) {
sessionStorage.removeItem(SESSION_STORAGE_KEYS.TIMER_START_TIME);
} else {
sessionStorage.setItem(SESSION_STORAGE_KEYS.TIMER_START_TIME, time.toString());
}
},
isRunning: () => sessionStorage.getItem(SESSION_STORAGE_KEYS.TIMER_IS_RUNNING) === 'true',
setRunning: running => sessionStorage.setItem(SESSION_STORAGE_KEYS.TIMER_IS_RUNNING, running ? 'true' : 'false'),
getAccumulated: () => {
const time = sessionStorage.getItem(SESSION_STORAGE_KEYS.TIMER_ACCUMULATED);
return time ? parseInt(time) : 0;
},
setAccumulated: time => sessionStorage.setItem(SESSION_STORAGE_KEYS.TIMER_ACCUMULATED, time.toString()),
// Timer control methods
start: () => {
if (!AppServices.Timer.isRunning()) {
AppServices.Timer.setStartTime(Date.now());
AppServices.Timer.setRunning(true);
TL.debug('TIMER', 'Timer started');
}
},
pause: () => {
if (AppServices.Timer.isRunning()) {
const startTime = AppServices.Timer.getStartTime();
const currentTime = Date.now();
const elapsed = currentTime - startTime;
const accumulated = AppServices.Timer.getAccumulated();
AppServices.Timer.setAccumulated(accumulated + elapsed);
AppServices.Timer.setRunning(false);
AppServices.Timer.setStartTime(null);
}
},
stop: () => {
AppServices.Timer.pause();
AppServices.Timer.setAccumulated(0);
},
getDisplay: () => {
let totalTime = AppServices.Timer.getAccumulated();
if (AppServices.Timer.isRunning()) {
const startTime = AppServices.Timer.getStartTime();
if (startTime) {
const currentTime = Date.now();
totalTime += (currentTime - startTime);
}
}
const hours = Math.floor(totalTime / 3600000);
const minutes = Math.floor((totalTime % 3600000) / 60000);
const seconds = Math.floor((totalTime % 60000) / 1000);
const display = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
TL.debug('TIMER', `Display: ${display}, Running: ${AppServices.Timer.isRunning()}, Accumulated: ${totalTime}`);
return display;
},
initialize: () => {
AppServices.Timer.setAccumulated(0);
AppServices.Timer.setRunning(false);
AppServices.Timer.setStartTime(null);
TL.debug('TIMER', 'Timer initialized');
}
},
// ====== HEARTBEAT SYSTEM ======
heartbeat: async () => {
if (!AppServices.isReadyToRun()) return;
try {
const status = {
logged_in: AppState.getIsLoggedIn(),
current_page: AppState.getCurrentPage(),
bot_status: AppSession.getSessionStatus(),
current_task: AppSession.getCurrentTask(),
current_task_data: AppSession.getCurrentTaskData(),
current_step: AppSession.getCurrentStep(),
};
// TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`);
const start = performance.now();
const res = await BAF.ping(status);
const end = performance.now();
const duration = Math.round(end - start);
if (res.ok) {
AppState.update({ server_last_seen: new Date(), server_latency: duration });
}
return status;
} catch (e) {
TL.error('HEARTBEAT', e.message);
return null;
}
},
handleNewTask: async () => {
const data = {
logged_in: AppState.getIsLoggedIn(),
current_page: AppState.getCurrentPage(),
bot_status: AppSession.getSessionStatus(),
current_task: AppSession.getCurrentTask(),
current_task_data: AppSession.getCurrentTaskData(),
current_step: AppSession.getCurrentStep(),
};
const taskResponse = await BAF.getTask(data);
TL.debug('APP-SERVICES', `Task: ${JSON.stringify(taskResponse, null, 2)}`);
if (taskResponse.ok) {
const task = taskResponse.data;
AppSession.setCurrentTask(task.type);
AppSession.setCurrentTaskData(task.data || null);
switch (task.type) {
case BAF_TASKS.NO_TASK:
TL.debug('APP-TASK', "No task. Sleep now");
break;
case BAF_TASKS.LOGIN:
AppTasks.login("start", task.data);
break;
default:
TL.debug('APP-TASK', `Unknown task 🛑`);
AppSession.setSessionStatus(AppEnums.BOT_STATUS.ERROR);
break;
}
}
},
resumeTask: async () => {
TL.debug('APP-SERVICES', 'Resume task');
const task = AppSession.getCurrentTask();
const data = AppSession.getCurrentTaskData();
const step = AppSession.getCurrentStep();
switch (task) {
case BAF_TASKS.LOGIN:
AppTasks.login(step, data);
break;
};
},
// ====== BOT STATUS CONTROL ======
Status: {
play: () => {
AppServices.Timer.start();
AppSession.setSessionStatus(AppEnums.BOT_STATUS.WAITING_FOR_TASK);
},
pause: () => {
AppServices.Timer.pause();
AppSession.setSessionStatus(AppEnums.BOT_STATUS.PAUSE);
},
stop: () => {
AppServices.Timer.stop();
AppSession.setCurrentTask(BAF_TASKS.NO_TASK);
AppSession.setCurrentTaskData(null);
AppSession.setCurrentStep(BAF_TASKS.NO_STEP);
AppSession.setSessionStatus(AppEnums.BOT_STATUS.STOP);
}
},
start: async () => {
while (true) {
switch (AppSession.getSessionStatus()) {
case AppEnums.BOT_STATUS.INITIALIZING:
TL.debug('APP-SERVICES', 'Bot is initializing, waiting...');
break;
case AppEnums.BOT_STATUS.STOP:
TL.debug('APP-SERVICES', 'Bot is stopped, skipping task check');
break;
case AppEnums.BOT_STATUS.PAUSE:
TL.debug('APP-SERVICES', 'Bot is paused, skipping task check');
break;
case AppEnums.BOT_STATUS.WAITING_FOR_TASK:
await AppServices.handleNewTask();
break;
case AppEnums.BOT_STATUS.PERFORMING_TASK:
await AppServices.resumeTask();
break;
case AppEnums.BOT_STATUS.ERROR:
TL.debug('APP-SERVICES', 'Bot is in error state, skipping task check');
break;
default:
TL.debug('APP-SERVICES', 'Bot is in unknown state, skipping task check');
break;
}
await TL.delay(CONFIG.TASK_INTERVAL);
}
}
};
// Start the application
try {
await AppServices.initialize();
} catch (error) {
TL.error('MAIN', 'Failed to initialize app:', error);
TL.error('MAIN', 'Error stack:', error.stack);
}
TL.log('MAIN', 'App started');
await AppServices.start();
})();