1878 lines
70 KiB
JavaScript
1878 lines
70 KiB
JavaScript
// ==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';
|
||
|
||
// ====== CONFIGURATION ======
|
||
const CONFIG = {
|
||
heartbeat_interval: 10000,
|
||
task_poll_interval: 10000,
|
||
is_debug: true,
|
||
servers: {
|
||
local: { label: '🏠 Local', url: 'http://localhost:3000' },
|
||
prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' },
|
||
}
|
||
};
|
||
|
||
// ====== STATE MANAGEMENT ======
|
||
class AppState {
|
||
constructor() {
|
||
this.data = {
|
||
// Server configuration
|
||
server_type: 'prod',
|
||
server_url: null,
|
||
|
||
// Page state
|
||
current_page: null,
|
||
is_logged_in: false,
|
||
|
||
// Bot status
|
||
bot_status: BOT_STATUS.WAITING,
|
||
|
||
// Task state
|
||
current_task: null,
|
||
task_data: null,
|
||
current_step: null,
|
||
step_data: null,
|
||
|
||
// UI state
|
||
is_loading: false,
|
||
error_message: null,
|
||
|
||
// Popup state
|
||
popup_position: 4, // 1: top-left, 2: top-right, 3: bottom-left, 4: bottom-right
|
||
popup_visible: true
|
||
};
|
||
|
||
this.observers = new Map();
|
||
this.initialized = false;
|
||
}
|
||
|
||
// Initialize state from storage and page detection
|
||
async initialize() {
|
||
if (this.initialized) return;
|
||
this.initialized = true;
|
||
|
||
// Load from storage
|
||
this.data.server_type = await STORAGE.getServerType();
|
||
this.data.server_url = CONFIG.servers[this.data.server_type] || CONFIG.servers.prod;
|
||
this.data.bot_status = await STORAGE.getBotStatus();
|
||
this.data.popup_position = await STORAGE.getPopupPosition();
|
||
this.data.popup_visible = await STORAGE.getPopupVisible();
|
||
|
||
// Detect current page and login state
|
||
this.data.current_page = BINANCE.detectPage();
|
||
this.data.is_logged_in = await BINANCE.isLoggedIn();
|
||
|
||
TL.debug('STATE', 'Initialized', this.data);
|
||
}
|
||
|
||
getData() {
|
||
return { ...this.data };
|
||
}
|
||
|
||
// Update state with partial update
|
||
async update(updates) {
|
||
const oldData = { ...this.data };
|
||
|
||
Object.assign(this.data, updates);
|
||
|
||
await this.notifyObservers(oldData, this.data);
|
||
|
||
TL.debug('STATE', 'Updated', updates);
|
||
}
|
||
|
||
subscribe(key, callback) {
|
||
if (!this.observers.has(key)) {
|
||
this.observers.set(key, []);
|
||
}
|
||
this.observers.get(key).push(callback);
|
||
}
|
||
|
||
unsubscribe(key, callback) {
|
||
const callbacks = this.observers.get(key);
|
||
if (callbacks) {
|
||
const index = callbacks.indexOf(callback);
|
||
if (index > -1) {
|
||
callbacks.splice(index, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Notify all observers
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ====== STORAGE MODULE ======
|
||
const STORAGE = {
|
||
key_token: 'baf-agent-token',
|
||
key_server_type: 'baf-server-type',
|
||
key_bot_status: 'baf-bot-status',
|
||
key_navigation_state: 'baf-navigation-state',
|
||
key_popup_position: 'baf-popup-position',
|
||
key_popup_visible: 'baf-popup-visible',
|
||
|
||
getToken: () => GM_getValue(STORAGE.key_token, ''),
|
||
setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')),
|
||
|
||
getServerType: () => GM_getValue(STORAGE.key_server_type, 'prod'),
|
||
setServerType: (type) => GM_setValue(STORAGE.key_server_type, type),
|
||
|
||
getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.WAITING),
|
||
setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status),
|
||
|
||
getNavigationState: () => {
|
||
try {
|
||
return JSON.parse(GM_getValue(STORAGE.key_navigation_state, 'null'));
|
||
} catch {
|
||
return null;
|
||
}
|
||
},
|
||
|
||
setNavigationState: (state) => {
|
||
GM_setValue(STORAGE.key_navigation_state, JSON.stringify(state));
|
||
},
|
||
|
||
clearNavigationState: () => {
|
||
GM_setValue(STORAGE.key_navigation_state, 'null');
|
||
},
|
||
|
||
getPopupPosition: () => GM_getValue(STORAGE.key_popup_position, 4),
|
||
setPopupPosition: (position) => GM_setValue(STORAGE.key_popup_position, position),
|
||
|
||
getPopupVisible: () => GM_getValue(STORAGE.key_popup_visible, true),
|
||
setPopupVisible: (visible) => GM_setValue(STORAGE.key_popup_visible, visible)
|
||
};
|
||
|
||
// ====== UTILITY MODULE ======
|
||
const TL = {
|
||
debug: (tag, msg, ...args) => CONFIG.is_debug && GM_log(`[TL] [${tag}]\n${msg}`, ...args),
|
||
log: (tag, msg, ...args) => GM_log(`[TL] [${tag}]\n${msg}`, ...args),
|
||
warn: (tag, msg, ...args) => GM_log(`[TL] [WARN] [${tag}] ⚠️\n${msg}`, ...args),
|
||
error: (tag, msg, ...args) => GM_log(`[TL] [ERROR] [${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)),
|
||
|
||
// Guard against double execution during navigation
|
||
guardDoubleRun: async (ctx, stepLogic) => {
|
||
// Check if we're currently navigating
|
||
if (ctx.step_data.navigating) {
|
||
TL.debug('STEP', 'Currently navigating, skipping step execution');
|
||
return;
|
||
}
|
||
|
||
// Check if this step was already completed after navigation
|
||
if (ctx.step_data.navigation_complete && ctx.step_data.last_completed_step === ctx.current_step_name) {
|
||
TL.debug('STEP', 'Step already completed after navigation, skipping');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await stepLogic();
|
||
|
||
// Mark step as completed (unless navigation was triggered)
|
||
if (!ctx.step_data.navigating) {
|
||
ctx.step_data.last_completed_step = ctx.current_step_name;
|
||
}
|
||
} catch (error) {
|
||
TL.error('STEP', `Error in step ${ctx.current_step_name}:`, error);
|
||
throw error;
|
||
}
|
||
},
|
||
};
|
||
|
||
// DOM helpers
|
||
TL.dom = {
|
||
isVisible: (el) => {
|
||
if (!el) return false;
|
||
const cs = getComputedStyle(el);
|
||
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
|
||
return (el.offsetWidth + el.offsetHeight) > 0;
|
||
},
|
||
|
||
isDisabled: (el) => {
|
||
return el?.disabled === true || el?.getAttribute('aria-disabled') === 'true';
|
||
},
|
||
|
||
isInViewport: (el) => {
|
||
if (!el) return false;
|
||
const rect = el.getBoundingClientRect();
|
||
return (
|
||
rect.top >= 0 &&
|
||
rect.left >= 0 &&
|
||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||
);
|
||
},
|
||
|
||
click: (el) => {
|
||
if (el && typeof el.click === 'function') el.click();
|
||
},
|
||
|
||
scrollToView: (el, behavior = 'smooth') => {
|
||
el?.scrollIntoView?.({ behavior, block: 'center' });
|
||
},
|
||
};
|
||
|
||
// Network helpers
|
||
TL.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,
|
||
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: reject,
|
||
ontimeout: () => reject(new Error('GM_xmlhttpRequest timeout')),
|
||
});
|
||
});
|
||
}
|
||
};
|
||
|
||
// ====== BAF API MODULE ======
|
||
const BAF = {
|
||
getServer: async () => {
|
||
const data = APP_STATE.getData();
|
||
return CONFIG.servers[data.server_type] || CONFIG.servers.prod;
|
||
},
|
||
|
||
getHost: async () => {
|
||
const s = await BAF.getServer();
|
||
return s.url;
|
||
},
|
||
|
||
request: async (method, path, { params, body, headers } = {}) => {
|
||
const base = await 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 || {});
|
||
const token = await STORAGE.getToken();
|
||
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);
|
||
},
|
||
|
||
get: (path, params, init = {}) => BAF.request('GET', path, { params, headers: init.headers }),
|
||
post: (path, body, init = {}) => BAF.request('POST', path, { body, headers: init.headers }),
|
||
|
||
ping: (status) => BAF.post('/agent/ping', status),
|
||
|
||
getTask: (status) => BAF.get('/agent/task', status),
|
||
submitTaskResult: (taskId, result) => BAF.post(`/agent/tasks/${taskId}/result`, result),
|
||
};
|
||
|
||
// ====== BINANCE MODULE ======
|
||
|
||
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'
|
||
};
|
||
|
||
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 = {
|
||
detectPage: () => {
|
||
if (!SafetyGuard.check('detectPage')) return BINANCE_PAGES.IGNORE;
|
||
|
||
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;
|
||
}
|
||
|
||
// Log current URL details
|
||
const msg = `Current URL: ${window.location.href}\n` +
|
||
`Hostname: ${hostname}, Pathname: ${pathname}\n` +
|
||
`window.top === window : ${window.top === window}\n`;
|
||
|
||
// Check patterns in order (most specific first)
|
||
for (const pattern of hostConfig.patterns) {
|
||
if (pathname.includes(pattern.includes)) {
|
||
TL.debug('PAGE_DETECT', msg +
|
||
`Matched pattern: ${pattern.includes} -> ${pattern.page}`);
|
||
return pattern.page;
|
||
}
|
||
}
|
||
|
||
TL.debug('PAGE_DETECT', msg + `No pattern matched: ${hostname}${pathname}`);
|
||
return BINANCE_PAGES.UNKNOWN;
|
||
},
|
||
|
||
isOnLoginPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.LOGIN,
|
||
isOnAlphaSwapPage: (contractAddress) => {
|
||
const page = APP_STATE.getData().current_page;
|
||
return page === BINANCE_PAGES.ALPHA_SWAP && (!contractAddress || window.location.pathname.includes(contractAddress));
|
||
},
|
||
isOnAlphaOrderHistoryPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.ALPHA_ORDER_HISTORY,
|
||
isOnWalletEarnPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.WALLET_EARN,
|
||
isOnWalletFundingPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.WALLET_FUNDING,
|
||
isOnWalletSpotPage: () => APP_STATE.getData().current_page === BINANCE_PAGES.WALLET_SPOT,
|
||
|
||
detectLoginState: () => {
|
||
if (!SafetyGuard.check('detectLoginState')) return null;
|
||
|
||
if (window.top !== window) {
|
||
TL.debug('BINANCE-LOGIN', 'In iframe - cannot determine login state');
|
||
return null;
|
||
}
|
||
|
||
if (BINANCE.isOnLoginPage()) {
|
||
return false;
|
||
}
|
||
|
||
// otherwise
|
||
// if (BINANCE.isOnAlphaSwapPage()) {
|
||
|
||
// 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 += `dashboard: ${dashboardLink}, accountIcon: ${accountIcon}, walletBtn: ${walletBtn}\n`;
|
||
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.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.detectLoginState() ?? false;
|
||
TL.debug(`BINANCE-LOGIN`, `isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`);
|
||
return fallback;
|
||
},
|
||
|
||
navigateToLogin: () => {
|
||
const loginBtn = document.querySelector('#toLoginPage');
|
||
if (TL.dom.isVisible?.(loginBtn)) {
|
||
TL.dom.click(loginBtn);
|
||
}
|
||
},
|
||
|
||
navigateToAlphaSwap: (contractAddress) => {
|
||
if (BINANCE.isOnAlphaSwapPage(contractAddress)) return;
|
||
window.location.href = contractAddress
|
||
? `https://www.binance.com/en/alpha/bsc/${contractAddress}`
|
||
: `https://www.binance.com/en/alpha`;
|
||
},
|
||
|
||
navigateToAlphaOrderHistory: () => {
|
||
if (!BINANCE.isOnAlphaOrderHistoryPage()) {
|
||
window.location.href = 'https://www.binance.com/en/my/orders/alpha/orderhistory';
|
||
}
|
||
},
|
||
|
||
navigateToWalletEarn: () => {
|
||
if (!BINANCE.isOnWalletEarnPage()) {
|
||
window.location.href = 'https://www.binance.com/en/my/wallet/account/earn';
|
||
}
|
||
},
|
||
|
||
navigateToWalletFunding: () => {
|
||
if (!BINANCE.isOnWalletFundingPage()) {
|
||
window.location.href = 'https://www.binance.com/en/my/wallet/funding';
|
||
}
|
||
},
|
||
|
||
navigateToWalletSpot: () => {
|
||
if (!BINANCE.isOnWalletSpotPage()) {
|
||
window.location.href = 'https://www.binance.com/en/my/wallet/account/main';
|
||
}
|
||
}
|
||
};
|
||
|
||
// ====== TASK & STEP SYSTEM ======
|
||
|
||
// Task Types Enum
|
||
const TASK_TYPES = {
|
||
LOGIN: 'login',
|
||
GET_ORDER_HISTORY: 'get_order_history',
|
||
GET_BALANCE: 'get_balance',
|
||
SWAP: 'swap',
|
||
NO_TASK: 'no_task'
|
||
};
|
||
|
||
// Bot Status Enum
|
||
const BOT_STATUS = {
|
||
WAITING: 'waiting', // 💤 waiting for new task
|
||
RUNNING: 'running', // ▶️ đang chạy task
|
||
PAUSED: 'paused', // ⏸️ đang pause
|
||
STOPPED: 'stopped' // ⏹️ đã stop
|
||
};
|
||
|
||
// Login Method Enum
|
||
const LOGIN_METHOD = {
|
||
QR_CODE: 'qr_code',
|
||
};
|
||
|
||
// Order Type Enum
|
||
const ORDER_TYPE = {
|
||
SWAP: 'swap',
|
||
LIMIT: 'limit',
|
||
MARKET: 'market'
|
||
};
|
||
|
||
// Order Status Enum
|
||
const ORDER_STATUS = {
|
||
COMPLETED: 'completed',
|
||
PENDING: 'pending',
|
||
CANCELLED: 'cancelled'
|
||
};
|
||
|
||
// Balance Format Enum
|
||
const BALANCE_FORMAT = {
|
||
JSON: 'json',
|
||
CSV: 'csv'
|
||
};
|
||
|
||
// Gas Fee Type Enum
|
||
const GAS_FEE_TYPE = {
|
||
AUTO: 'auto',
|
||
FAST: 'fast',
|
||
SLOW: 'slow'
|
||
};
|
||
|
||
const STEP_TYPES = {
|
||
// Login task steps
|
||
NAVIGATE_TO_LOGIN: 'navigate_to_login',
|
||
SELECT_QR_CODE: 'select_qr_code',
|
||
SEND_TO_SERVER: 'send_to_server',
|
||
WAIT_FOR_LOGIN: 'wait_for_login',
|
||
REPORT_RESULT: 'report_result',
|
||
|
||
// Order history task steps
|
||
NAVIGATE_TO_ORDER_HISTORY: 'navigate_to_order_history',
|
||
EXTRACT_ORDER_DATA: 'extract_order_data',
|
||
SEND_ORDER_DATA: 'send_order_data',
|
||
|
||
// Balance task steps
|
||
NAVIGATE_TO_BALANCE: 'navigate_to_balance',
|
||
EXTRACT_BALANCE_DATA: 'extract_balance_data',
|
||
SEND_BALANCE_DATA: 'send_balance_data',
|
||
|
||
// Swap task steps
|
||
NAVIGATE_TO_SWAP: 'navigate_to_swap',
|
||
FILL_SWAP_FORM: 'fill_swap_form',
|
||
CONFIRM_SWAP: 'confirm_swap',
|
||
WAIT_FOR_SWAP: 'wait_for_swap',
|
||
|
||
// Common steps
|
||
WAIT: 'wait',
|
||
ERROR_HANDLING: 'error_handling'
|
||
};
|
||
|
||
// ====== STEP FUNCTIONS ======
|
||
|
||
// Login Step Functions
|
||
const LoginSteps = {
|
||
matchNavigateToLogin: (url, ctx) => {
|
||
// // const isOnLoginPage = url.includes("/login");
|
||
// // const isLoggedIn = ctx?.is_logged_in || false;
|
||
|
||
// // // Only navigate to login if:
|
||
// // // 1. Already on login page, OR
|
||
// // // 2. Not logged in AND on a page that doesn't require login
|
||
// // if (isOnLoginPage) {
|
||
// // return true;
|
||
// // }
|
||
|
||
// // if (!isLoggedIn) {
|
||
// // // Check if we're on a page that requires login
|
||
// // // const currentPage = BINANCE.detectPage();
|
||
// // const requiresLoginPages = [
|
||
// // BINANCE_PAGES.ALPHA_SWAP,
|
||
// // BINANCE_PAGES.ALPHA_ORDER_HISTORY,
|
||
// // BINANCE_PAGES.WALLET_EARN,
|
||
// // BINANCE_PAGES.WALLET_FUNDING,
|
||
// // BINANCE_PAGES.WALLET_SPOT
|
||
// // ];
|
||
|
||
// // // If we're on a page that requires login but not logged in,
|
||
// // // don't navigate to login (let the page handle it)
|
||
// // if (requiresLoginPages.includes(currentPage)) {
|
||
// // return false;
|
||
// // }
|
||
|
||
// // // Only navigate to login if on a public page
|
||
// return true;
|
||
// }
|
||
|
||
return false;
|
||
},
|
||
|
||
runNavigateToLogin: async (ctx) => {
|
||
await TL.guardDoubleRun(ctx, async () => {
|
||
// TL.log('STEP', 'NavigateToLogin: Starting login process');
|
||
|
||
// if (!BINANCE.isOnLoginPage()) {
|
||
// BINANCE.navigateToLogin();
|
||
// ctx.step_data.page_navigated = true;
|
||
// }
|
||
|
||
// ctx.step_data.page_loaded = true;
|
||
// ctx.step_data.start_time = Date.now();
|
||
});
|
||
},
|
||
|
||
matchSelectQRCode: (url, ctx) => {
|
||
return url.includes("/login") &&
|
||
ctx?.step_data?.page_loaded &&
|
||
ctx?.task_data?.method === LOGIN_METHOD.QR_CODE;
|
||
},
|
||
|
||
runSelectQRCode: async (ctx) => {
|
||
// await TL.guardDoubleRun(ctx, async () => {
|
||
// TL.log('STEP', 'SelectQRCode: Selecting QR code method');
|
||
|
||
// const qrButton = document.querySelector('[data-testid="qr-code-tab"], .qr-code-btn, .qr-tab');
|
||
// if (qrButton) {
|
||
// qrButton.click();
|
||
// ctx.step_data.qr_selected = true;
|
||
// TL.debug('STEP', 'QR code method selected');
|
||
// } else {
|
||
// TL.debug('STEP', 'QR code button not found, may already be selected');
|
||
// ctx.step_data.qr_selected = true;
|
||
// }
|
||
// });
|
||
},
|
||
|
||
matchWaitForLogin: (url, ctx) => {
|
||
return !url.includes("/login") && ctx?.step_data?.qr_selected;
|
||
},
|
||
|
||
runWaitForLogin: async (ctx) => {
|
||
// is
|
||
}
|
||
};
|
||
|
||
// Order History Step Functions
|
||
const OrderHistorySteps = {
|
||
//TODO: Implement
|
||
|
||
};
|
||
|
||
// Balance Step Functions
|
||
const BalanceSteps = {
|
||
//TODO: Implement
|
||
};
|
||
|
||
// ====== TASK DEFINITIONS ======
|
||
const TASK_DEFINITIONS = {
|
||
[TASK_TYPES.LOGIN]: {
|
||
id: TASK_TYPES.LOGIN,
|
||
steps: [
|
||
{
|
||
name: "NavigateToLogin",
|
||
match: LoginSteps.matchNavigateToLogin,
|
||
run: LoginSteps.runNavigateToLogin
|
||
},
|
||
{
|
||
name: "SelectQRCode",
|
||
match: LoginSteps.matchSelectQRCode,
|
||
run: LoginSteps.runSelectQRCode
|
||
},
|
||
{
|
||
name: "WaitForLogin",
|
||
match: LoginSteps.matchWaitForLogin,
|
||
run: LoginSteps.runWaitForLogin
|
||
}
|
||
]
|
||
},
|
||
|
||
[TASK_TYPES.GET_ORDER_HISTORY]: {
|
||
id: TASK_TYPES.GET_ORDER_HISTORY,
|
||
steps: [
|
||
//TODO: Implement
|
||
]
|
||
},
|
||
|
||
[TASK_TYPES.GET_BALANCE]: {
|
||
id: TASK_TYPES.GET_BALANCE,
|
||
steps: [
|
||
//TODO: Implement
|
||
]
|
||
},
|
||
|
||
[TASK_TYPES.SWAP]: {
|
||
id: TASK_TYPES.SWAP,
|
||
steps: [
|
||
//TODO: Implement
|
||
]
|
||
},
|
||
|
||
[TASK_TYPES.NO_TASK]: {
|
||
id: TASK_TYPES.NO_TASK,
|
||
steps: [
|
||
//TODO: Implement
|
||
]
|
||
}
|
||
};
|
||
|
||
// ====== STEP RUNNER ======
|
||
class StepRunner {
|
||
constructor() {
|
||
this.currentStepIndex = 0;
|
||
this.context = null;
|
||
}
|
||
|
||
// Create context for task execution
|
||
createContext(task, taskData) {
|
||
return {
|
||
task_id: task.id,
|
||
task_type: task.type,
|
||
task_data: taskData,
|
||
step_data: {},
|
||
is_logged_in: APP_STATE.getData().is_logged_in,
|
||
current_page: APP_STATE.getData().current_page,
|
||
current_step_name: null, // Will be set by executeCurrentStep
|
||
done: (result) => this.completeTask(result),
|
||
goto: (url) => this.navigateTo(url),
|
||
wait: (ms) => this.wait(ms),
|
||
retry: (fn, maxAttempts = 3) => this.retry(fn, maxAttempts)
|
||
};
|
||
}
|
||
|
||
// Navigate to URL
|
||
navigateTo(url) {
|
||
if (!url || typeof url !== 'string') {
|
||
TL.error('STEP', 'Invalid URL provided for navigation:', url);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Handle different URL formats
|
||
let targetUrl = url;
|
||
|
||
if (url.startsWith('/')) {
|
||
// Relative URL - append to current domain
|
||
targetUrl = window.location.origin + url;
|
||
} else if (!url.startsWith('http')) {
|
||
// Relative URL without leading slash
|
||
targetUrl = window.location.origin + '/' + url;
|
||
}
|
||
// Absolute URL (starts with http) - use as is
|
||
|
||
TL.debug('STEP', `Navigating to: ${targetUrl}`);
|
||
|
||
// Mark that we're navigating to avoid double execution
|
||
this.context.step_data.navigating = true;
|
||
this.context.step_data.navigation_start = Date.now();
|
||
this.context.step_data.navigation_target = targetUrl;
|
||
|
||
// Save navigation state to storage for persistence across page reload
|
||
STORAGE.setNavigationState({
|
||
navigating: true,
|
||
navigation_start: this.context.step_data.navigation_start,
|
||
navigation_target: targetUrl,
|
||
task_id: this.context.task_id,
|
||
step_index: this.currentStepIndex
|
||
});
|
||
|
||
window.location.href = targetUrl;
|
||
|
||
} catch (error) {
|
||
TL.error('STEP', 'Navigation failed:', error);
|
||
// Clear navigation state on error
|
||
this.context.step_data.navigating = false;
|
||
delete this.context.step_data.navigation_start;
|
||
delete this.context.step_data.navigation_target;
|
||
}
|
||
}
|
||
|
||
// Wait for specified milliseconds
|
||
async wait(ms) {
|
||
if (ms && ms > 0) {
|
||
TL.debug('STEP', `Waiting for ${ms}ms`);
|
||
await TL.delay(ms);
|
||
}
|
||
}
|
||
|
||
// Retry function with exponential backoff
|
||
async retry(fn, maxAttempts = 3) {
|
||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||
try {
|
||
return await fn();
|
||
} catch (error) {
|
||
TL.debug('STEP', `Attempt ${attempt} failed:`, error.message);
|
||
|
||
if (attempt === maxAttempts) {
|
||
throw error;
|
||
}
|
||
|
||
// Exponential backoff: 1s, 2s, 4s
|
||
const delay = Math.pow(2, attempt - 1) * 1000;
|
||
await this.wait(delay);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Complete task and return to ready state
|
||
async completeTask(result) {
|
||
try {
|
||
TL.log('STEP', `Task completed with result:`, result);
|
||
|
||
const data = APP_STATE.getData();
|
||
if (data.current_task) {
|
||
// Submit result to server
|
||
try {
|
||
await BAF.submitTaskResult(data.current_task.id, result);
|
||
TL.debug('STEP', 'Task result submitted to server successfully');
|
||
} catch (error) {
|
||
TL.error('STEP', 'Failed to submit task result to server:', error);
|
||
// Continue with cleanup even if server submission fails
|
||
}
|
||
}
|
||
|
||
// Return to waiting state
|
||
await APP_STATE.update({
|
||
current_task: null,
|
||
task_data: null,
|
||
current_step: null,
|
||
step_data: null,
|
||
bot_status: BOT_STATUS.WAITING,
|
||
is_loading: false,
|
||
error_message: result.success ? null : result.error
|
||
});
|
||
|
||
// Reset step runner state
|
||
this.currentStepIndex = 0;
|
||
this.context = null;
|
||
|
||
// Clear navigation state
|
||
STORAGE.clearNavigationState();
|
||
|
||
// Check for next task after a short delay
|
||
setTimeout(() => TaskRunner.checkForNewTasks(), 1000);
|
||
|
||
} catch (error) {
|
||
TL.error('STEP', 'Error during task completion:', error);
|
||
// Force return to ready state even if cleanup fails
|
||
await APP_STATE.update({
|
||
bot_status: BOT_STATUS.WAITING,
|
||
is_loading: false,
|
||
error_message: 'Task completion error: ' + error.message
|
||
});
|
||
}
|
||
}
|
||
|
||
// Execute current step
|
||
async executeCurrentStep() {
|
||
if (!SafetyGuard.check('executeCurrentStep')) return;
|
||
|
||
const data = APP_STATE.getData();
|
||
if (!data.current_task || !this.context) {
|
||
TL.debug('STEP', 'No current task or context available');
|
||
return;
|
||
}
|
||
|
||
// Check if bot is paused or stopped
|
||
if (data.bot_status === BOT_STATUS.PAUSED) {
|
||
TL.debug('STEP', 'Bot is paused - skipping step execution');
|
||
return;
|
||
}
|
||
|
||
if (data.bot_status === BOT_STATUS.STOPPED) {
|
||
TL.debug('STEP', 'Bot is stopped - skipping step execution');
|
||
return;
|
||
}
|
||
|
||
const taskDefinition = TASK_DEFINITIONS[data.current_task.type];
|
||
if (!taskDefinition) {
|
||
TL.error('STEP', `No task definition found for type: ${data.current_task.type}`);
|
||
await this.completeTask({ success: false, error: 'Unknown task type' });
|
||
return;
|
||
}
|
||
|
||
const currentStep = taskDefinition.steps[this.currentStepIndex];
|
||
if (!currentStep) {
|
||
TL.error('STEP', `No step found at index: ${this.currentStepIndex}`);
|
||
await this.completeTask({ success: false, error: 'Step not found' });
|
||
return;
|
||
}
|
||
|
||
// Set current step name in context
|
||
this.context.current_step_name = currentStep.name;
|
||
|
||
const url = window.location.href;
|
||
TL.debug('STEP', `Checking step: ${currentStep.name} at URL: ${url}`);
|
||
|
||
if (currentStep.match(url, this.context)) {
|
||
try {
|
||
TL.log('STEP', `Executing step: ${currentStep.name}`);
|
||
|
||
// Update context with current state
|
||
this.context.is_logged_in = APP_STATE.getData().is_logged_in;
|
||
this.context.current_page = APP_STATE.getData().current_page;
|
||
|
||
// Execute step (guardDoubleRun is already wrapped in step functions)
|
||
await currentStep.run(this.context);
|
||
|
||
// Check if navigation was triggered
|
||
if (this.context.step_data.navigating) {
|
||
TL.debug('STEP', 'Navigation triggered, stopping execution');
|
||
return;
|
||
}
|
||
|
||
// Move to next step
|
||
this.currentStepIndex++;
|
||
await APP_STATE.update({
|
||
current_step: currentStep.name,
|
||
step_data: this.context.step_data
|
||
});
|
||
|
||
// Continue with next step if available
|
||
if (this.currentStepIndex < taskDefinition.steps.length) {
|
||
TL.debug('STEP', `Moving to next step: ${this.currentStepIndex + 1}/${taskDefinition.steps.length}`);
|
||
setTimeout(() => this.executeCurrentStep(), 100);
|
||
} else {
|
||
// All steps completed
|
||
TL.log('STEP', 'All steps completed successfully');
|
||
await this.completeTask({ success: true, message: 'All steps completed' });
|
||
}
|
||
} catch (error) {
|
||
TL.error('STEP', `Error executing step ${currentStep.name}:`, error);
|
||
await this.completeTask({ success: false, error: error.message });
|
||
}
|
||
} else {
|
||
TL.debug('STEP', `Step ${currentStep.name} conditions not met, waiting for page change...`);
|
||
// Don't retry immediately, wait for page change or state change
|
||
}
|
||
}
|
||
|
||
// Resume task from saved state
|
||
async resumeTask() {
|
||
const data = APP_STATE.getData();
|
||
if (!data.current_task) {
|
||
return;
|
||
}
|
||
|
||
// Check for navigation state
|
||
const navigationState = STORAGE.getNavigationState();
|
||
if (navigationState && navigationState.navigating) {
|
||
await this.handleNavigationResume(navigationState);
|
||
return;
|
||
}
|
||
|
||
// Create context from saved state
|
||
this.context = this.createContext(data.current_task, data.task_data);
|
||
this.context.step_data = data.step_data || {};
|
||
|
||
// Resume from current step
|
||
await this.executeCurrentStep();
|
||
}
|
||
|
||
// Handle navigation resume
|
||
async handleNavigationResume(navigationState) {
|
||
TL.log('STEP', 'Resuming after navigation:', navigationState);
|
||
|
||
const data = APP_STATE.getData();
|
||
if (!data.current_task || data.current_task.id !== navigationState.task_id) {
|
||
TL.warn('STEP', 'Task ID mismatch, clearing navigation state');
|
||
STORAGE.clearNavigationState();
|
||
return;
|
||
}
|
||
|
||
// Check if we're on the target page
|
||
const currentUrl = window.location.href;
|
||
const targetUrl = navigationState.navigation_target;
|
||
|
||
if (currentUrl.includes(targetUrl) || this.isSamePage(currentUrl, targetUrl)) {
|
||
TL.log('STEP', 'Successfully navigated to target page');
|
||
|
||
// Clear navigation state
|
||
STORAGE.clearNavigationState();
|
||
|
||
// Create context and continue
|
||
this.context = this.createContext(data.current_task, data.task_data);
|
||
this.context.step_data = data.step_data || {};
|
||
this.currentStepIndex = navigationState.step_index;
|
||
|
||
// Mark navigation as complete
|
||
this.context.step_data.navigating = false;
|
||
this.context.step_data.navigation_complete = true;
|
||
this.context.step_data.navigation_complete_time = Date.now();
|
||
|
||
// Continue with next step
|
||
this.currentStepIndex++;
|
||
await this.executeCurrentStep();
|
||
|
||
} else {
|
||
// Still navigating or navigation failed
|
||
const elapsed = Date.now() - navigationState.navigation_start;
|
||
const timeout = 10000; // 10 seconds timeout
|
||
|
||
if (elapsed > timeout) {
|
||
TL.error('STEP', 'Navigation timeout, clearing state');
|
||
STORAGE.clearNavigationState();
|
||
await this.completeTask({
|
||
success: false,
|
||
error: 'Navigation timeout'
|
||
});
|
||
} else {
|
||
TL.debug('STEP', 'Still navigating, waiting...');
|
||
// Wait a bit more for navigation to complete
|
||
setTimeout(() => this.handleNavigationResume(navigationState), 500);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Check if two URLs represent the same page
|
||
isSamePage(url1, url2) {
|
||
try {
|
||
const parsed1 = new URL(url1);
|
||
const parsed2 = new URL(url2);
|
||
|
||
// Compare hostname and pathname
|
||
return parsed1.hostname === parsed2.hostname &&
|
||
parsed1.pathname === parsed2.pathname;
|
||
} catch {
|
||
// Fallback to string comparison
|
||
return url1 === url2;
|
||
}
|
||
}
|
||
|
||
// Validate task definition
|
||
validateTaskDefinition(taskType) {
|
||
const taskDefinition = TASK_DEFINITIONS[taskType];
|
||
if (!taskDefinition) {
|
||
throw new Error(`No task definition found for type: ${taskType}`);
|
||
}
|
||
|
||
if (!taskDefinition.steps || !Array.isArray(taskDefinition.steps)) {
|
||
throw new Error(`Invalid steps array for task type: ${taskType}`);
|
||
}
|
||
|
||
taskDefinition.steps.forEach((step, index) => {
|
||
if (!step.name || !step.match || !step.run) {
|
||
throw new Error(`Invalid step at index ${index} for task type: ${taskType}`);
|
||
}
|
||
});
|
||
|
||
return taskDefinition;
|
||
}
|
||
|
||
// Start new task
|
||
async startTask(task, taskData) {
|
||
try {
|
||
TL.log('STEP', `Starting task: ${task.type}`, task);
|
||
|
||
// Validate task definition
|
||
this.validateTaskDefinition(task.type);
|
||
|
||
this.currentStepIndex = 0;
|
||
this.context = this.createContext(task, taskData);
|
||
|
||
await APP_STATE.update({
|
||
current_task: task,
|
||
task_data: taskData,
|
||
current_step: null,
|
||
step_data: {},
|
||
bot_status: BOT_STATUS.RUNNING,
|
||
is_loading: true,
|
||
error_message: null
|
||
});
|
||
|
||
// Start executing steps
|
||
await this.executeCurrentStep();
|
||
|
||
} catch (error) {
|
||
TL.error('STEP', 'Failed to start task:', error);
|
||
await this.completeTask({ success: false, error: error.message });
|
||
}
|
||
}
|
||
}
|
||
|
||
// ====== TASK RUNNER ======
|
||
const TaskRunner = {
|
||
stepRunner: new StepRunner(),
|
||
|
||
// Helper function to validate enum values
|
||
validateEnum(value, enumObj, defaultValue) {
|
||
const validValues = Object.values(enumObj);
|
||
if (validValues.includes(value)) {
|
||
return value;
|
||
}
|
||
TL.debug('TASK', `Invalid enum value: ${value}, using default: ${defaultValue}`);
|
||
return defaultValue;
|
||
},
|
||
|
||
// Helper function to validate and merge task data with defaults
|
||
validateTaskData(taskType, taskData = {}) {
|
||
const mergedData = { ...taskData };
|
||
|
||
// Validate enum fields if present
|
||
if (taskType === TASK_TYPES.LOGIN && mergedData.method) {
|
||
mergedData.method = this.validateEnum(mergedData.method, LOGIN_METHOD, LOGIN_METHOD.QR_CODE);
|
||
}
|
||
|
||
if (taskType === TASK_TYPES.GET_ORDER_HISTORY) {
|
||
if (mergedData.order_types) {
|
||
mergedData.order_types = mergedData.order_types.map(type =>
|
||
this.validateEnum(type, ORDER_TYPE, ORDER_TYPE.SWAP)
|
||
);
|
||
}
|
||
if (mergedData.status) {
|
||
mergedData.status = mergedData.status.map(status =>
|
||
this.validateEnum(status, ORDER_STATUS, ORDER_STATUS.COMPLETED)
|
||
);
|
||
}
|
||
}
|
||
|
||
if (taskType === TASK_TYPES.GET_BALANCE && mergedData.format) {
|
||
mergedData.format = this.validateEnum(mergedData.format, BALANCE_FORMAT, BALANCE_FORMAT.JSON);
|
||
}
|
||
|
||
if (taskType === TASK_TYPES.SWAP && mergedData.gas_fee && typeof mergedData.gas_fee === 'string') {
|
||
mergedData.gas_fee = this.validateEnum(mergedData.gas_fee, GAS_FEE_TYPE, GAS_FEE_TYPE.AUTO);
|
||
}
|
||
|
||
TL.debug('TASK', `Validated task data for ${taskType}:`, mergedData);
|
||
return mergedData;
|
||
},
|
||
|
||
// Helper function to get current task data
|
||
getCurrentTaskData() {
|
||
const data = APP_STATE.getData();
|
||
return data.task_data || {};
|
||
},
|
||
|
||
// Helper function to update task data
|
||
async updateTaskData(newData) {
|
||
const currentData = this.getCurrentTaskData();
|
||
const updatedData = { ...currentData, ...newData };
|
||
await APP_STATE.update({ task_data: updatedData });
|
||
return updatedData;
|
||
},
|
||
|
||
// Helper function to check if current task is of specific type
|
||
isCurrentTaskType(taskType) {
|
||
const data = APP_STATE.getData();
|
||
return data.current_task?.type === taskType;
|
||
},
|
||
|
||
// Helper function to get current task type
|
||
getCurrentTaskType() {
|
||
const data = APP_STATE.getData();
|
||
return data.current_task?.type || null;
|
||
},
|
||
async checkForNewTasks() {
|
||
if (!SafetyGuard.check('checkForNewTasks')) return;
|
||
|
||
try {
|
||
const data = APP_STATE.getData();
|
||
if (data.bot_status !== BOT_STATUS.WAITING && data.bot_status !== BOT_STATUS.RUNNING) {
|
||
TL.debug('TASK', 'Bot not ready for new tasks, status:', data.bot_status);
|
||
return;
|
||
}
|
||
|
||
const response = await BAF.getTask();
|
||
if (response.ok && response.data.task) {
|
||
const task = response.data.task;
|
||
TL.log('TASK', `Received new task: ${task.type}`, task);
|
||
|
||
// Validate task data
|
||
const validatedTaskData = TaskRunner.validateTaskData(task.type, task.data);
|
||
|
||
// Start task using StepRunner
|
||
await this.stepRunner.startTask(task, validatedTaskData);
|
||
} else if (response.ok && response.data.no_task) {
|
||
TL.debug('TASK', 'No new tasks available');
|
||
// Sleep for interval then check again
|
||
setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval);
|
||
}
|
||
} catch (error) {
|
||
TL.error('TASK', 'Failed to check for new tasks:', error);
|
||
// Retry after interval
|
||
setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval);
|
||
}
|
||
},
|
||
|
||
// Continue current step (called by observers)
|
||
async continueCurrentStep() {
|
||
await this.stepRunner.executeCurrentStep();
|
||
}
|
||
};
|
||
|
||
// ====== STATE OBSERVERS ======
|
||
const StateWatchers = {
|
||
// Storage observer - saves state changes to storage
|
||
async onServerTypeChange(oldValue, newValue, data) {
|
||
TL.debug('OBSERVER', `Server mode changed: ${oldValue} -> ${newValue}`);
|
||
await STORAGE.setServerType(newValue);
|
||
|
||
// Update server info
|
||
await APP_STATE.update({
|
||
server_url: CONFIG.servers[newValue] || CONFIG.servers.prod
|
||
});
|
||
|
||
// Reload page after a short delay
|
||
setTimeout(() => location.reload(), 500);
|
||
},
|
||
|
||
async onBotStatusChange(oldValue, newValue, data) {
|
||
TL.debug('OBSERVER', `Bot status changed: ${oldValue} -> ${newValue}`);
|
||
await STORAGE.setBotStatus(newValue);
|
||
|
||
// Handle status-specific logic
|
||
if (newValue === BOT_STATUS.WAITING) {
|
||
// Start checking for new tasks
|
||
TaskRunner.checkForNewTasks();
|
||
} else if (newValue === BOT_STATUS.RUNNING) {
|
||
// Continue with current task or start new task checking
|
||
if (data.current_task) {
|
||
// Continue current task
|
||
TaskRunner.continueCurrentStep();
|
||
} else {
|
||
// Start checking for new tasks
|
||
TaskRunner.checkForNewTasks();
|
||
}
|
||
} else if (newValue === BOT_STATUS.PAUSED) {
|
||
// Pause current operations
|
||
TL.debug('OBSERVER', 'Bot paused - stopping task operations');
|
||
} else if (newValue === BOT_STATUS.STOPPED) {
|
||
// Stop all operations
|
||
TL.debug('OBSERVER', 'Bot stopped - clearing current task');
|
||
await APP_STATE.update({
|
||
current_task: null,
|
||
task_data: null,
|
||
current_step: null,
|
||
step_data: null,
|
||
is_loading: false,
|
||
error_message: null
|
||
});
|
||
}
|
||
|
||
// Refresh menu to show new status
|
||
setTimeout(() => createGM_Menu(), 100);
|
||
},
|
||
|
||
async onCurrentPageChange(oldValue, newValue, data) {
|
||
TL.debug('OBSERVER', `Page changed: ${oldValue} -> ${newValue}`);
|
||
|
||
// Handle page-specific logic for current task
|
||
if (data.current_task && data.bot_status === BOT_STATUS.RUNNING) {
|
||
// Continue with current step based on page change
|
||
await TaskRunner.continueCurrentStep();
|
||
}
|
||
},
|
||
|
||
// Popup observers
|
||
async onPopupPositionChange(oldValue, newValue, data) {
|
||
TL.debug('OBSERVER', `Popup position changed: ${oldValue} -> ${newValue}`);
|
||
UI.statusOverlay.updatePosition();
|
||
},
|
||
|
||
async onPopupVisibleChange(oldValue, newValue, data) {
|
||
TL.debug('OBSERVER', `Popup visibility changed: ${oldValue} -> ${newValue}`);
|
||
if (newValue) {
|
||
UI.statusOverlay.show();
|
||
} else {
|
||
UI.statusOverlay.hide();
|
||
}
|
||
},
|
||
|
||
// General state change observer for popup content updates
|
||
async onAnyStateChange(oldValue, newValue, data) {
|
||
// Update popup content when any relevant state changes
|
||
if (UI.statusOverlay.element && data.popup_visible) {
|
||
UI.statusOverlay.updateContent();
|
||
}
|
||
}
|
||
};
|
||
|
||
// ====== MENU SYSTEM ======
|
||
let menuCreated = false;
|
||
|
||
async function createGM_Menu() {
|
||
if (menuCreated) {
|
||
// Clear existing menu items if recreating
|
||
// Note: GM_registerMenuCommand doesn't have a clear method,
|
||
// so we'll just set a flag to prevent recreation
|
||
return;
|
||
}
|
||
menuCreated = true;
|
||
GM_registerMenuCommand('🔍 Safety Guard Status', () => {
|
||
const status = SafetyGuard.getStatus();
|
||
alert(`Safety Guard Status:\n\n${status.message}\nEnabled: ${status.enabled}`);
|
||
});
|
||
|
||
GM_registerMenuCommand(' - ✅ Enable Operations', () => {
|
||
SafetyGuard.enable();
|
||
});
|
||
|
||
GM_registerMenuCommand(' - 🚨 Block Operations', () => {
|
||
SafetyGuard.disable();
|
||
});
|
||
|
||
const data = APP_STATE.getData();
|
||
const curSrv = data.server_url;
|
||
|
||
GM_registerMenuCommand(`Server: ${curSrv.label} (${curSrv.url})`, async () => {
|
||
try {
|
||
const next = (data.server_type === 'local') ? 'prod' : 'local';
|
||
await APP_STATE.update({ server_type: next });
|
||
|
||
const nsv = CONFIG.servers[next];
|
||
const msg = `Switched to ${nsv.label} (${nsv.url})`;
|
||
TL.debug(`BAF`, msg);
|
||
TL.noti('BAF Server Switched', msg);
|
||
} catch (e) {
|
||
TL.error('BAF', 'switch server error', e);
|
||
TL.noti('BAF Server Switched', `Switch server error: ${e.message}`);
|
||
}
|
||
});
|
||
|
||
GM_registerMenuCommand('Token', async () => {
|
||
try {
|
||
const curToken = await STORAGE.getToken();
|
||
const input = prompt('Bearer token:', curToken || '');
|
||
if (input !== null && input.trim() && input.trim() !== curToken) {
|
||
await STORAGE.setToken(input);
|
||
}
|
||
const s = await BAF.getServer();
|
||
const res = await BAF.ping();
|
||
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);
|
||
}
|
||
});
|
||
|
||
// Bot Status Menu với icon và tên task
|
||
const getStatusDisplay = () => {
|
||
const data = APP_STATE.getData();
|
||
const taskName = data.current_task?.type || '';
|
||
|
||
switch (data.bot_status) {
|
||
case BOT_STATUS.WAITING:
|
||
return `💤 Waiting for new task`;
|
||
case BOT_STATUS.RUNNING:
|
||
return `▶️ Running: ${taskName}`;
|
||
case BOT_STATUS.PAUSED:
|
||
return `⏸️ Paused: ${taskName}`;
|
||
case BOT_STATUS.STOPPED:
|
||
return `⏹️ Stopped`;
|
||
default:
|
||
return `❓ Unknown status`;
|
||
}
|
||
};
|
||
|
||
// Popup toggle menu
|
||
const popupData = APP_STATE.getData();
|
||
const popupStatus = popupData.popup_visible ? '👁️ Hide Popup' : '👁️ Show Popup';
|
||
GM_registerMenuCommand(popupStatus, () => {
|
||
UI.statusOverlay.toggle();
|
||
});
|
||
|
||
|
||
|
||
|
||
// Debug tools
|
||
GM_registerMenuCommand('🔍 Current Status', () => {
|
||
const detectedPage = APP_STATE.getData().current_page;
|
||
const isLoggedIn = APP_STATE.getData().is_logged_in;
|
||
const currentUrl = window.location.href;
|
||
|
||
const status = `Page: ${detectedPage}\nLogin: ${isLoggedIn ? 'Yes' : 'No'}\nURL: ${currentUrl}`;
|
||
|
||
console.log('Current Status:', status);
|
||
alert(status);
|
||
});
|
||
|
||
GM_registerMenuCommand('🔐 Debug Login State', () => {
|
||
const isInIframe = window.top !== window;
|
||
const loginState = BINANCE.detectLoginState();
|
||
|
||
// Check all possible login indicators
|
||
const loginIndicators = {
|
||
'In iframe': isInIframe,
|
||
'Login button visible': TL.dom.isVisible?.(document.querySelector('#toLoginPage')),
|
||
'Register button visible': TL.dom.isVisible?.(document.querySelector('#toRegisterPage')),
|
||
'Account icon found': !!document.querySelector('.header-account-icon'),
|
||
'Deposit button found': !!document.querySelector('.deposit-btn'),
|
||
'Wallet button found': !!document.querySelector('#ba-wallet'),
|
||
'User menu found': !!document.querySelector('.header-dropdown-menu'),
|
||
'Dashboard link found': !!document.querySelector('a[href*="/my/dashboard"]'),
|
||
'Detected login state': loginState
|
||
};
|
||
|
||
const debugInfo = Object.entries(loginIndicators)
|
||
.map(([key, value]) => `${key}: ${value}`)
|
||
.join('\n');
|
||
|
||
console.log('=== LOGIN STATE DEBUG ===');
|
||
console.log(debugInfo);
|
||
alert(`Login State Debug:\n\n${debugInfo}`);
|
||
});
|
||
|
||
GM_registerMenuCommand('🔍 Debug URL Sources', () => {
|
||
const mainUrl = window.location.href;
|
||
const frames = Array.from(document.querySelectorAll('iframe')).map(frame => frame.src);
|
||
const popups = window.opener ? window.opener.location.href : null;
|
||
|
||
const debugInfo = `
|
||
Main Window: ${mainUrl}
|
||
Frames: ${frames.length > 0 ? frames.join('\n') : 'None'}
|
||
Popup Opener: ${popups || 'None'}
|
||
`;
|
||
|
||
console.log('=== URL SOURCES DEBUG ===');
|
||
console.log(debugInfo);
|
||
alert(`URL Sources Debug:\n\n${debugInfo}`);
|
||
});
|
||
|
||
|
||
}
|
||
|
||
// ====== HEARTBEAT ======
|
||
async function heartbeat_report() {
|
||
if (!SafetyGuard.check('heartbeat_report')) return null;
|
||
|
||
try {
|
||
const data = APP_STATE.getData();
|
||
const status = {
|
||
logged_in: data.is_logged_in,
|
||
current_page: data.current_page,
|
||
bot_status: data.bot_status,
|
||
current_task: data.current_task?.type || null,
|
||
task_data: data.task_data || null,
|
||
current_step: data.current_step || null,
|
||
};
|
||
|
||
// TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`);
|
||
await BAF.ping(status);
|
||
return status;
|
||
} catch (e) {
|
||
TL.error('HEARTBEAT', e.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ====== UI SYSTEM ======
|
||
const UI = {
|
||
// ====== STATUS OVERLAY ======
|
||
statusOverlay: {
|
||
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 status overlay element
|
||
create: () => {
|
||
if (UI.statusOverlay.element) {
|
||
document.body.removeChild(UI.statusOverlay.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 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 data = APP_STATE.getData();
|
||
const newPosition = (data.popup_position % 4) + 1;
|
||
APP_STATE.update({ popup_position: newPosition });
|
||
STORAGE.setPopupPosition(newPosition);
|
||
UI.statusOverlay.updatePosition();
|
||
});
|
||
positionBtn.addEventListener('mouseenter', () => {
|
||
positionBtn.style.transform = 'scale(1.5)';
|
||
positionBtn.style.background = '#0056b3';
|
||
});
|
||
positionBtn.addEventListener('mouseleave', () => {
|
||
positionBtn.style.transform = 'scale(1)';
|
||
positionBtn.style.background = '#007bff';
|
||
});
|
||
|
||
overlay.appendChild(positionBtn);
|
||
UI.statusOverlay.element = overlay;
|
||
document.body.appendChild(overlay);
|
||
|
||
return overlay;
|
||
},
|
||
|
||
// Update overlay position
|
||
updatePosition: () => {
|
||
if (!UI.statusOverlay.element) return;
|
||
|
||
const data = APP_STATE.getData();
|
||
const positionCSS = UI.statusOverlay.getPositionCSS(data.popup_position);
|
||
|
||
Object.assign(UI.statusOverlay.element.style, positionCSS);
|
||
},
|
||
|
||
// Update overlay content
|
||
updateContent: () => {
|
||
if (!UI.statusOverlay.element) return;
|
||
|
||
const data = APP_STATE.getData();
|
||
const taskName = data.current_task?.type || 'No task';
|
||
const stepName = data.current_step || 'No step';
|
||
|
||
// Get status display
|
||
let statusDisplay = '';
|
||
switch (data.bot_status) {
|
||
case BOT_STATUS.WAITING:
|
||
statusDisplay = '💤 Waiting';
|
||
break;
|
||
case BOT_STATUS.RUNNING:
|
||
statusDisplay = '▶️ Running';
|
||
break;
|
||
case BOT_STATUS.PAUSED:
|
||
statusDisplay = '⏸️ Paused';
|
||
break;
|
||
case BOT_STATUS.STOPPED:
|
||
statusDisplay = '⏹️ Stopped';
|
||
break;
|
||
default:
|
||
statusDisplay = '❓ Unknown';
|
||
}
|
||
|
||
// Get server info
|
||
const serverLabel = data.server_url?.label || 'Unknown';
|
||
|
||
// Get page info
|
||
const pageDisplay = data.current_page || 'unknown';
|
||
|
||
// Get login status
|
||
const loginStatus = data.is_logged_in ? '✅ Logged in' : '❌ Not logged in';
|
||
|
||
// Get SafetyGuard status
|
||
const safetyStatus = SafetyGuard.getStatus();
|
||
const safetyDisplay = safetyStatus.enabled ? '🛡️ Safe' : '🚨 Blocked';
|
||
|
||
// Build content
|
||
const content = `
|
||
<div style="margin-bottom: 8px;">
|
||
<strong>BAF Agent Status</strong>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #007bff;">${statusDisplay}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #28a745;">${safetyDisplay}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #ffc107;">Task: ${taskName}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #17a2b8;">Step: ${stepName}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #6f42c1;">Page: ${pageDisplay}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #fd7e14;">${loginStatus}</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: #e83e8c;">Server: ${serverLabel}</span>
|
||
</div>
|
||
${data.error_message ? `<div style="color: #dc3545; margin-top: 4px;">Error: ${data.error_message}</div>` : ''}
|
||
`;
|
||
|
||
// Update content (excluding position button)
|
||
const contentDiv = UI.statusOverlay.element.querySelector('.baf-content') ||
|
||
document.createElement('div');
|
||
contentDiv.className = 'baf-content';
|
||
contentDiv.innerHTML = content;
|
||
|
||
if (!UI.statusOverlay.element.querySelector('.baf-content')) {
|
||
UI.statusOverlay.element.appendChild(contentDiv);
|
||
}
|
||
},
|
||
|
||
// Show status overlay
|
||
show: () => {
|
||
if (!UI.statusOverlay.element) {
|
||
UI.statusOverlay.create();
|
||
}
|
||
|
||
UI.statusOverlay.updatePosition();
|
||
UI.statusOverlay.updateContent();
|
||
UI.statusOverlay.element.style.display = 'block';
|
||
},
|
||
|
||
// Hide status overlay
|
||
hide: () => {
|
||
if (UI.statusOverlay.element) {
|
||
UI.statusOverlay.element.style.display = 'none';
|
||
}
|
||
},
|
||
|
||
// Toggle status overlay
|
||
toggle: () => {
|
||
const data = APP_STATE.getData();
|
||
if (data.popup_visible) {
|
||
UI.statusOverlay.hide();
|
||
APP_STATE.update({ popup_visible: false });
|
||
STORAGE.setPopupVisible(false);
|
||
} else {
|
||
UI.statusOverlay.show();
|
||
APP_STATE.update({ popup_visible: true });
|
||
STORAGE.setPopupVisible(true);
|
||
}
|
||
},
|
||
|
||
// Initialize status overlay
|
||
init: () => {
|
||
const data = APP_STATE.getData();
|
||
if (data.popup_visible) {
|
||
UI.statusOverlay.show();
|
||
}
|
||
}
|
||
},
|
||
|
||
// ====== NOTIFICATION SYSTEM ======
|
||
notification: {
|
||
// Show success notification
|
||
success: (title, message, duration = 3000) => {
|
||
TL.noti(title, message, duration);
|
||
},
|
||
|
||
// Show error notification
|
||
error: (title, message, duration = 5000) => {
|
||
TL.noti(`❌ ${title}`, message, duration);
|
||
},
|
||
|
||
// Show warning notification
|
||
warning: (title, message, duration = 4000) => {
|
||
TL.noti(`⚠️ ${title}`, message, duration);
|
||
},
|
||
|
||
// Show info notification
|
||
info: (title, message, duration = 3000) => {
|
||
TL.noti(`ℹ️ ${title}`, message, duration);
|
||
}
|
||
},
|
||
|
||
// ====== LOADING INDICATOR ======
|
||
loading: {
|
||
element: null,
|
||
|
||
// Show loading indicator
|
||
show: (message = 'Loading...') => {
|
||
if (UI.loading.element) {
|
||
UI.loading.hide();
|
||
}
|
||
|
||
const loading = document.createElement('div');
|
||
loading.id = 'baf-loading-indicator';
|
||
loading.style.cssText = `
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
z-index: 1000000;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
color: white;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
font-size: 14px;
|
||
text-align: center;
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
`;
|
||
loading.innerHTML = `
|
||
<div style="margin-bottom: 10px;">⏳</div>
|
||
<div>${message}</div>
|
||
`;
|
||
|
||
UI.loading.element = loading;
|
||
document.body.appendChild(loading);
|
||
},
|
||
|
||
// Hide loading indicator
|
||
hide: () => {
|
||
if (UI.loading.element) {
|
||
document.body.removeChild(UI.loading.element);
|
||
UI.loading.element = null;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// ====== SAFETY GUARD SYSTEM ======
|
||
const SafetyGuard = {
|
||
status: true, // true = safe to proceed, false = blocked
|
||
|
||
// Check if it's safe to proceed with an action
|
||
check: (actionName = 'unknown') => {
|
||
return SafetyGuard.status; // true = safe to proceed
|
||
},
|
||
|
||
// Enable safety (allow actions)
|
||
enable: () => {
|
||
SafetyGuard.status = true;
|
||
TL.noti('✅ Safety Guard', 'Bot operations enabled', 3000);
|
||
TL.debug('SAFETY_GUARD', 'Safety guard enabled');
|
||
},
|
||
|
||
// Disable safety (block all actions)
|
||
disable: () => {
|
||
SafetyGuard.status = false;
|
||
TL.noti('🚨 Safety Guard', 'Bot operations blocked', 10000);
|
||
TL.debug('SAFETY_GUARD', 'Safety guard disabled');
|
||
},
|
||
|
||
// Get current status
|
||
getStatus: () => ({
|
||
enabled: SafetyGuard.status,
|
||
message: SafetyGuard.status ? '✅ Operations Enabled' : '🚨 Operations Blocked'
|
||
})
|
||
};
|
||
|
||
|
||
// ====== INITIALIZATION ======
|
||
const APP_STATE = new AppState();
|
||
|
||
async function initialize() {
|
||
// Initialize state
|
||
await APP_STATE.initialize();
|
||
|
||
// Register observers
|
||
APP_STATE.subscribe('server_type', StateWatchers.onServerTypeChange);
|
||
APP_STATE.subscribe('bot_status', StateWatchers.onBotStatusChange);
|
||
APP_STATE.subscribe('current_page', StateWatchers.onCurrentPageChange);
|
||
APP_STATE.subscribe('popup_position', StateWatchers.onPopupPositionChange);
|
||
APP_STATE.subscribe('popup_visible', StateWatchers.onPopupVisibleChange);
|
||
|
||
// Register general state observer for popup content updates
|
||
APP_STATE.subscribe('current_task', StateWatchers.onAnyStateChange);
|
||
APP_STATE.subscribe('current_step', StateWatchers.onAnyStateChange);
|
||
APP_STATE.subscribe('error_message', StateWatchers.onAnyStateChange);
|
||
APP_STATE.subscribe('bot_status', StateWatchers.onAnyStateChange);
|
||
|
||
// Create menu
|
||
await createGM_Menu();
|
||
|
||
// Initialize popup helper
|
||
UI.statusOverlay.init();
|
||
|
||
// Start heartbeat
|
||
setInterval(heartbeat_report, CONFIG.heartbeat_interval);
|
||
|
||
// Update popup content periodically
|
||
setInterval(() => {
|
||
if (UI.statusOverlay.element && APP_STATE.getData().popup_visible) {
|
||
UI.statusOverlay.updateContent();
|
||
}
|
||
}, 1000); // Update every 2 seconds
|
||
|
||
// Welcome message
|
||
const ping_res = await BAF.ping();
|
||
const data = APP_STATE.getData();
|
||
const res =
|
||
`Server: ${data.server_url.label} (${data.server_url.url})\n` +
|
||
`Status: ${ping_res.ok ? 'Connected ✅' : 'Failed ❌'} (${ping_res.status})\n` +
|
||
`Page: ${data.current_page || 'unknown'} (${window.location.href})\n` +
|
||
`Bot Status: ${data.bot_status}`;
|
||
|
||
TL.log(`BAF`, res);
|
||
|
||
// Resume task if performing or start new task checking if ready
|
||
if (data.bot_status === BOT_STATUS.RUNNING && data.current_task) {
|
||
TL.log('INIT', 'Resuming interrupted task');
|
||
TaskRunner.stepRunner.resumeTask();
|
||
} else if (data.bot_status === BOT_STATUS.WAITING) {
|
||
TaskRunner.checkForNewTasks();
|
||
} else if (data.bot_status === BOT_STATUS.PAUSED) {
|
||
TL.log('INIT', 'Bot is paused - waiting for resume');
|
||
} else if (data.bot_status === BOT_STATUS.STOPPED) {
|
||
TL.log('INIT', 'Bot is stopped - waiting for start');
|
||
}
|
||
}
|
||
|
||
// Start the application
|
||
await initialize();
|
||
})(); |