Initial commit: POE2 automated trade bot

Monitors pathofexile.com/trade2 for new listings, travels to seller
hideouts, buys items from public stash tabs, and stores them.

Includes persistent C# OCR daemon for fast screen capture + Windows
native OCR, web dashboard for managing trade links and settings,
and full game automation via Win32 SendInput.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Boki 2026-02-10 14:03:47 -05:00
commit 41d174195e
28 changed files with 6449 additions and 0 deletions

View file

@ -0,0 +1,187 @@
import { EventEmitter } from 'events';
import { logger } from '../util/logger.js';
import type { ConfigStore, SavedLink } from './ConfigStore.js';
export interface TradeLink {
id: string;
url: string;
name: string;
label: string;
active: boolean;
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;
};
}
export class BotController extends EventEmitter {
private paused = false;
private links: Map<string, TradeLink> = new Map();
private _state = 'IDLE';
private tradesCompleted = 0;
private tradesFailed = 0;
private startTime = Date.now();
private store: ConfigStore;
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 = ''): 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 link: TradeLink = {
id,
url,
name: name || savedLink?.name || '',
label,
active: savedLink?.active !== undefined ? savedLink.active : true,
addedAt: new Date().toISOString(),
};
this.links.set(id, link);
this.store.addLink(url, link.name);
logger.info({ id, url, name: link.name, active: link.active }, '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 });
}
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,
},
};
}
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);
}
}