import { EventEmitter } from 'events'; import { logger } from '../util/logger.js'; import type { LinkMode, PostAction } from '../types.js'; import type { ConfigStore, SavedLink } from './ConfigStore.js'; export interface TradeLink { id: string; url: string; name: string; label: string; active: boolean; mode: LinkMode; postAction: PostAction; addedAt: string; } export interface BotStatus { paused: boolean; state: string; links: TradeLink[]; tradesCompleted: number; tradesFailed: number; uptime: number; settings: { poe2LogPath: string; poe2WindowTitle: string; travelTimeoutMs: number; waitForMoreItemsMs: number; betweenTradesDelayMs: number; }; inventory?: { grid: boolean[][]; items: { row: number; col: number; w: number; h: number }[]; free: number; }; } export class BotController extends EventEmitter { private paused = false; private links: Map = new Map(); private _state = 'IDLE'; private tradesCompleted = 0; private tradesFailed = 0; private startTime = Date.now(); private store: ConfigStore; private _inventory: BotStatus['inventory'] = undefined; constructor(store: ConfigStore) { super(); this.store = store; this.paused = store.settings.paused; } get isPaused(): boolean { return this.paused; } get state(): string { return this._state; } set state(s: string) { this._state = s; this.emit('state-change', s); } pause(): void { this.paused = true; this.store.setPaused(true); logger.info('Bot paused'); this.emit('paused'); } resume(): void { this.paused = false; this.store.setPaused(false); logger.info('Bot resumed'); this.emit('resumed'); } addLink(url: string, name: string = '', mode?: LinkMode, postAction?: PostAction): TradeLink { url = this.stripLive(url); const id = this.extractId(url); const label = this.extractLabel(url); // Check if we have saved state for this link const savedLink = this.store.links.find((l) => l.url === url); const resolvedMode = mode || savedLink?.mode || 'live'; const link: TradeLink = { id, url, name: name || savedLink?.name || '', label, active: savedLink?.active !== undefined ? savedLink.active : true, mode: resolvedMode, postAction: postAction || savedLink?.postAction || (resolvedMode === 'scrap' ? 'salvage' : 'stash'), addedAt: new Date().toISOString(), }; this.links.set(id, link); this.store.addLink(url, link.name, link.mode, link.postAction); logger.info({ id, url, name: link.name, active: link.active, mode: link.mode, postAction: link.postAction }, 'Trade link added'); this.emit('link-added', link); return link; } removeLink(id: string): void { const link = this.links.get(id); this.links.delete(id); if (link) { this.store.removeLink(link.url); } else { this.store.removeLinkById(id); } logger.info({ id }, 'Trade link removed'); this.emit('link-removed', id); } toggleLink(id: string, active: boolean): void { const link = this.links.get(id); if (!link) return; link.active = active; this.store.updateLinkById(id, { active }); logger.info({ id, active }, `Trade link ${active ? 'activated' : 'deactivated'}`); this.emit('link-toggled', { id, active, link }); } updateLinkName(id: string, name: string): void { const link = this.links.get(id); if (!link) return; link.name = name; this.store.updateLinkById(id, { name }); } updateLinkMode(id: string, mode: LinkMode): void { const link = this.links.get(id); if (!link) return; link.mode = mode; this.store.updateLinkById(id, { mode }); logger.info({ id, mode }, 'Trade link mode updated'); this.emit('link-mode-changed', { id, mode, link }); } updateLinkPostAction(id: string, postAction: PostAction): void { const link = this.links.get(id); if (!link) return; link.postAction = postAction; this.store.updateLinkById(id, { postAction }); logger.info({ id, postAction }, 'Trade link postAction updated'); this.emit('link-postaction-changed', { id, postAction, link }); } isLinkActive(searchId: string): boolean { const link = this.links.get(searchId); return link ? link.active : false; } getLinks(): TradeLink[] { return Array.from(this.links.values()); } recordTradeSuccess(): void { this.tradesCompleted++; this.emit('trade-completed'); } recordTradeFailure(): void { this.tradesFailed++; this.emit('trade-failed'); } getStatus(): BotStatus { const s = this.store.settings; return { paused: this.paused, state: this._state, links: this.getLinks(), tradesCompleted: this.tradesCompleted, tradesFailed: this.tradesFailed, uptime: Date.now() - this.startTime, settings: { poe2LogPath: s.poe2LogPath, poe2WindowTitle: s.poe2WindowTitle, travelTimeoutMs: s.travelTimeoutMs, waitForMoreItemsMs: s.waitForMoreItemsMs, betweenTradesDelayMs: s.betweenTradesDelayMs, }, inventory: this._inventory, }; } setInventory(inv: BotStatus['inventory']): void { this._inventory = inv; } getStore(): ConfigStore { return this.store; } private stripLive(url: string): string { return url.replace(/\/live\/?$/, ''); } private extractId(url: string): string { const parts = url.split('/'); return parts[parts.length - 1] || url; } private extractLabel(url: string): string { try { const urlObj = new URL(url); const parts = urlObj.pathname.split('/').filter(Boolean); const poe2Idx = parts.indexOf('poe2'); if (poe2Idx >= 0 && parts.length > poe2Idx + 2) { const league = decodeURIComponent(parts[poe2Idx + 1]); const searchId = parts[poe2Idx + 2]; return `${league} / ${searchId}`; } } catch { // fallback } return url.substring(0, 60); } }