switched to new way :)
This commit is contained in:
parent
b03a2a25f1
commit
f22d182c8f
30 changed files with 0 additions and 0 deletions
406
src-old/bot/Bot.ts
Normal file
406
src-old/bot/Bot.ts
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import { logger } from '../util/logger.js';
|
||||
import { LinkManager } from './LinkManager.js';
|
||||
import { GameController } from '../game/GameController.js';
|
||||
import { ScreenReader } from '../game/ScreenReader.js';
|
||||
import { ClientLogWatcher } from '../log/ClientLogWatcher.js';
|
||||
import { TradeMonitor } from '../trade/TradeMonitor.js';
|
||||
import { InventoryManager } from '../inventory/InventoryManager.js';
|
||||
import { TradeExecutor } from '../executor/TradeExecutor.js';
|
||||
import { TradeQueue } from '../executor/TradeQueue.js';
|
||||
import { ScrapExecutor } from '../executor/ScrapExecutor.js';
|
||||
import type { TradeLink } from './LinkManager.js';
|
||||
import type { ConfigStore } from './ConfigStore.js';
|
||||
import type { Config, LinkMode, PostAction } from '../types.js';
|
||||
import type { Page } from 'playwright';
|
||||
|
||||
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 Bot extends EventEmitter {
|
||||
private paused: boolean;
|
||||
private _state = 'IDLE';
|
||||
private tradesCompleted = 0;
|
||||
private tradesFailed = 0;
|
||||
private startTime = Date.now();
|
||||
private _inventory: BotStatus['inventory'] = undefined;
|
||||
private _started = false;
|
||||
|
||||
readonly links: LinkManager;
|
||||
readonly store: ConfigStore;
|
||||
readonly config: Config;
|
||||
|
||||
gameController!: GameController;
|
||||
screenReader!: ScreenReader;
|
||||
logWatcher!: ClientLogWatcher;
|
||||
tradeMonitor!: TradeMonitor;
|
||||
inventoryManager!: InventoryManager;
|
||||
tradeExecutor!: TradeExecutor;
|
||||
tradeQueue!: TradeQueue;
|
||||
scrapExecutors = new Map<string, ScrapExecutor>();
|
||||
|
||||
constructor(store: ConfigStore, config: Config) {
|
||||
super();
|
||||
this.store = store;
|
||||
this.config = config;
|
||||
this.paused = store.settings.paused;
|
||||
this.links = new LinkManager(store);
|
||||
}
|
||||
|
||||
get isReady(): boolean {
|
||||
return this._started;
|
||||
}
|
||||
|
||||
get isPaused(): boolean {
|
||||
return this.paused;
|
||||
}
|
||||
|
||||
get state(): string {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
set state(s: string) {
|
||||
if (this._state !== s) {
|
||||
this._state = s;
|
||||
this.emit('status-update');
|
||||
}
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this.paused = true;
|
||||
this.store.setPaused(true);
|
||||
logger.info('Bot paused');
|
||||
this.emit('status-update');
|
||||
}
|
||||
|
||||
resume(): void {
|
||||
this.paused = false;
|
||||
this.store.setPaused(false);
|
||||
logger.info('Bot resumed');
|
||||
this.emit('status-update');
|
||||
}
|
||||
|
||||
// --- Link operations (delegate to LinkManager + emit events) ---
|
||||
|
||||
addLink(url: string, name?: string, mode?: LinkMode, postAction?: PostAction): TradeLink {
|
||||
const link = this.links.addLink(url, name, mode, postAction);
|
||||
this.emit('link-added', link);
|
||||
this.emit('status-update');
|
||||
return link;
|
||||
}
|
||||
|
||||
removeLink(id: string): void {
|
||||
this.links.removeLink(id);
|
||||
this.emit('link-removed', id);
|
||||
this.emit('status-update');
|
||||
}
|
||||
|
||||
toggleLink(id: string, active: boolean): void {
|
||||
const link = this.links.toggleLink(id, active);
|
||||
if (link) {
|
||||
this.emit('link-toggled', { id, active, link });
|
||||
this.emit('status-update');
|
||||
}
|
||||
}
|
||||
|
||||
updateLinkName(id: string, name: string): void {
|
||||
this.links.updateName(id, name);
|
||||
this.emit('status-update');
|
||||
}
|
||||
|
||||
updateLinkMode(id: string, mode: LinkMode): void {
|
||||
const link = this.links.updateMode(id, mode);
|
||||
if (link) {
|
||||
this.emit('link-mode-changed', { id, mode, link });
|
||||
this.emit('status-update');
|
||||
}
|
||||
}
|
||||
|
||||
updateLinkPostAction(id: string, postAction: PostAction): void {
|
||||
const link = this.links.updatePostAction(id, postAction);
|
||||
if (link) {
|
||||
this.emit('link-postaction-changed', { id, postAction, link });
|
||||
this.emit('status-update');
|
||||
}
|
||||
}
|
||||
|
||||
isLinkActive(searchId: string): boolean {
|
||||
return this.links.isActive(searchId);
|
||||
}
|
||||
|
||||
updateSettings(updates: Record<string, unknown>): void {
|
||||
this.store.updateSettings(updates);
|
||||
this.emit('status-update');
|
||||
}
|
||||
|
||||
setInventory(inv: BotStatus['inventory']): void {
|
||||
this._inventory = inv;
|
||||
}
|
||||
|
||||
getStatus(): BotStatus {
|
||||
const s = this.store.settings;
|
||||
return {
|
||||
paused: this.paused,
|
||||
state: this._state,
|
||||
links: this.links.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,
|
||||
};
|
||||
}
|
||||
|
||||
/** Called by executor state callbacks to update bot state */
|
||||
updateExecutorState(): void {
|
||||
this._inventory = this.inventoryManager.getInventoryState();
|
||||
|
||||
const execState = this.tradeExecutor.getState();
|
||||
if (execState !== 'IDLE') {
|
||||
this.state = execState;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [, scrapExec] of this.scrapExecutors) {
|
||||
const scrapState = scrapExec.getState();
|
||||
if (scrapState !== 'IDLE') {
|
||||
this.state = scrapState;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.state = 'IDLE';
|
||||
}
|
||||
|
||||
// --- Startup / Shutdown ---
|
||||
|
||||
async start(cliUrls: string[], port: number): Promise<void> {
|
||||
this.screenReader = new ScreenReader();
|
||||
this.gameController = new GameController(this.config);
|
||||
|
||||
this.logWatcher = new ClientLogWatcher(this.config.poe2LogPath);
|
||||
await this.logWatcher.start();
|
||||
this.emit('log', 'info', 'Watching Client.txt for game events');
|
||||
|
||||
this.tradeMonitor = new TradeMonitor(this.config);
|
||||
await this.tradeMonitor.start(`http://localhost:${port}`);
|
||||
this.emit('log', 'info', 'Browser launched');
|
||||
|
||||
this.inventoryManager = new InventoryManager(
|
||||
this.gameController, this.screenReader, this.logWatcher, this.config,
|
||||
);
|
||||
|
||||
// Pre-warm OCR daemon + EasyOCR model in background (don't await yet)
|
||||
const ocrWarmup = this.screenReader.warmup().catch(err => {
|
||||
logger.warn({ err }, 'OCR warmup failed (will retry on first use)');
|
||||
});
|
||||
|
||||
// Check if already in hideout from log tail
|
||||
const alreadyInHideout = this.logWatcher.currentArea.toLowerCase().includes('hideout');
|
||||
|
||||
if (alreadyInHideout) {
|
||||
logger.info({ area: this.logWatcher.currentArea }, 'Already in hideout, skipping /hideout command');
|
||||
this.inventoryManager.setLocation(true);
|
||||
} else {
|
||||
this.emit('log', 'info', 'Sending /hideout command...');
|
||||
await this.gameController.focusGame();
|
||||
const arrivedHome = await this.inventoryManager.waitForAreaTransition(
|
||||
this.config.travelTimeoutMs,
|
||||
() => this.gameController.goToHideout(),
|
||||
);
|
||||
if (arrivedHome) {
|
||||
this.inventoryManager.setLocation(true);
|
||||
this.logWatcher.currentArea = 'Hideout';
|
||||
} else {
|
||||
this.inventoryManager.setLocation(true);
|
||||
this.logWatcher.currentArea = 'Hideout';
|
||||
logger.warn('Timed out waiting for hideout transition on startup (may already be in hideout)');
|
||||
}
|
||||
}
|
||||
this.state = 'IN_HIDEOUT';
|
||||
this.emit('log', 'info', 'In hideout, ready to trade');
|
||||
|
||||
// Ensure OCR warmup finished before proceeding to inventory scan
|
||||
await ocrWarmup;
|
||||
|
||||
// Clear leftover inventory
|
||||
this.emit('log', 'info', 'Checking inventory for leftover items...');
|
||||
await this.inventoryManager.clearToStash();
|
||||
this.emit('log', 'info', 'Inventory cleared');
|
||||
|
||||
// Create executors
|
||||
this.tradeExecutor = new TradeExecutor(
|
||||
this.gameController, this.screenReader, this.tradeMonitor,
|
||||
this.inventoryManager, this.config,
|
||||
);
|
||||
this.tradeExecutor.onStateChange = () => this.updateExecutorState();
|
||||
|
||||
this.tradeQueue = new TradeQueue(this.tradeExecutor, this.config);
|
||||
|
||||
// Collect all URLs: CLI args + saved links (deduped)
|
||||
const allUrls = new Set<string>([
|
||||
...cliUrls,
|
||||
...this.store.settings.links.map(l => l.url),
|
||||
]);
|
||||
|
||||
// Load links (direct, before wiring events)
|
||||
for (const url of allUrls) {
|
||||
const link = this.links.addLink(url);
|
||||
if (link.active) {
|
||||
await this.activateLink(link);
|
||||
} else {
|
||||
this.emit('log', 'info', `Loaded (inactive): ${link.name || link.label}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Wire events for subsequent UI-triggered changes
|
||||
this.wireEvents();
|
||||
|
||||
this._started = true;
|
||||
this.emit('log', 'info', `Loaded ${allUrls.size} trade link(s) from config`);
|
||||
logger.info('Bot started');
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
logger.info('Shutting down bot...');
|
||||
for (const [, scrapExec] of this.scrapExecutors) {
|
||||
await scrapExec.stop();
|
||||
}
|
||||
await this.screenReader.dispose();
|
||||
await this.tradeMonitor.stop();
|
||||
await this.logWatcher.stop();
|
||||
}
|
||||
|
||||
// --- Internal ---
|
||||
|
||||
private wireEvents(): void {
|
||||
this.on('link-added', (link: TradeLink) => {
|
||||
if (link.active) {
|
||||
this.activateLink(link).catch(err => {
|
||||
logger.error({ err }, 'Failed to activate link from event');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.on('link-removed', (id: string) => {
|
||||
this.deactivateLink(id).catch(err => {
|
||||
logger.error({ err }, 'Failed to deactivate link from event');
|
||||
});
|
||||
this.emit('log', 'info', `Removed search: ${id}`);
|
||||
});
|
||||
|
||||
this.on('link-toggled', (data: { id: string; active: boolean; link: TradeLink }) => {
|
||||
if (data.active) {
|
||||
this.activateLink(data.link).catch(err => {
|
||||
logger.error({ err }, 'Failed to activate link from toggle');
|
||||
});
|
||||
this.emit('log', 'info', `Activated: ${data.link.name || data.id}`);
|
||||
} else {
|
||||
this.deactivateLink(data.id).catch(err => {
|
||||
logger.error({ err }, 'Failed to deactivate link from toggle');
|
||||
});
|
||||
this.emit('log', 'info', `Deactivated: ${data.link.name || data.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.on('link-mode-changed', (data: { id: string; mode: string; link: TradeLink }) => {
|
||||
if (data.link.active) {
|
||||
this.deactivateLink(data.id).then(() => this.activateLink(data.link)).catch(err => {
|
||||
logger.error({ err }, 'Failed to restart link after mode change');
|
||||
});
|
||||
this.emit('log', 'info', `Mode changed to ${data.mode}: ${data.link.name || data.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.on('link-postaction-changed', (data: { id: string; postAction: string; link: TradeLink }) => {
|
||||
if (data.link.active) {
|
||||
this.deactivateLink(data.id).then(() => this.activateLink(data.link)).catch(err => {
|
||||
logger.error({ err }, 'Failed to restart link after postAction change');
|
||||
});
|
||||
this.emit('log', 'info', `Post-action changed to ${data.postAction}: ${data.link.name || data.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Trade monitor → trade queue
|
||||
this.tradeMonitor.on('new-listings', (data: { searchId: string; itemIds: string[]; page: Page }) => {
|
||||
if (this.isPaused) {
|
||||
this.emit('log', 'warn', `New listings (${data.itemIds.length}) skipped - bot paused`);
|
||||
return;
|
||||
}
|
||||
if (!this.isLinkActive(data.searchId)) return;
|
||||
|
||||
logger.info({ searchId: data.searchId, itemCount: data.itemIds.length }, 'New listings received, queuing trade...');
|
||||
this.emit('log', 'info', `New listings: ${data.itemIds.length} items from ${data.searchId}`);
|
||||
|
||||
this.tradeQueue.enqueue({
|
||||
searchId: data.searchId,
|
||||
itemIds: data.itemIds,
|
||||
whisperText: '',
|
||||
timestamp: Date.now(),
|
||||
tradeUrl: '',
|
||||
page: data.page,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async activateLink(link: TradeLink): Promise<void> {
|
||||
try {
|
||||
if (link.mode === 'scrap') {
|
||||
const scrapExec = new ScrapExecutor(
|
||||
this.gameController, this.screenReader, this.tradeMonitor,
|
||||
this.inventoryManager, this.config,
|
||||
);
|
||||
scrapExec.onStateChange = () => this.updateExecutorState();
|
||||
this.scrapExecutors.set(link.id, scrapExec);
|
||||
this.emit('log', 'info', `Scrap loop started: ${link.name || link.label}`);
|
||||
this.emit('status-update');
|
||||
|
||||
scrapExec.runScrapLoop(link.url, link.postAction).catch((err) => {
|
||||
logger.error({ err, linkId: link.id }, 'Scrap loop error');
|
||||
this.emit('log', 'error', `Scrap loop failed: ${link.name || link.label}`);
|
||||
this.scrapExecutors.delete(link.id);
|
||||
});
|
||||
} else {
|
||||
await this.tradeMonitor.addSearch(link.url);
|
||||
this.emit('log', 'info', `Monitoring: ${link.name || link.label}`);
|
||||
this.emit('status-update');
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ err, url: link.url }, 'Failed to activate link');
|
||||
this.emit('log', 'error', `Failed to activate: ${link.name || link.label}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async deactivateLink(id: string): Promise<void> {
|
||||
const scrapExec = this.scrapExecutors.get(id);
|
||||
if (scrapExec) {
|
||||
await scrapExec.stop();
|
||||
this.scrapExecutors.delete(id);
|
||||
}
|
||||
await this.tradeMonitor.pauseSearch(id);
|
||||
}
|
||||
}
|
||||
146
src-old/bot/ConfigStore.ts
Normal file
146
src-old/bot/ConfigStore.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { logger } from '../util/logger.js';
|
||||
import type { LinkMode, PostAction } from '../types.js';
|
||||
|
||||
export interface SavedLink {
|
||||
url: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
mode: LinkMode;
|
||||
postAction?: PostAction;
|
||||
addedAt: string;
|
||||
}
|
||||
|
||||
export interface SavedSettings {
|
||||
paused: boolean;
|
||||
links: SavedLink[];
|
||||
poe2LogPath: string;
|
||||
poe2WindowTitle: string;
|
||||
browserUserDataDir: string;
|
||||
travelTimeoutMs: number;
|
||||
stashScanTimeoutMs: number;
|
||||
waitForMoreItemsMs: number;
|
||||
betweenTradesDelayMs: number;
|
||||
dashboardPort: number;
|
||||
}
|
||||
|
||||
const DEFAULTS: SavedSettings = {
|
||||
paused: false,
|
||||
links: [],
|
||||
poe2LogPath: 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Path of Exile 2\\logs\\Client.txt',
|
||||
poe2WindowTitle: 'Path of Exile 2',
|
||||
browserUserDataDir: './browser-data',
|
||||
travelTimeoutMs: 15000,
|
||||
stashScanTimeoutMs: 10000,
|
||||
waitForMoreItemsMs: 20000,
|
||||
betweenTradesDelayMs: 5000,
|
||||
dashboardPort: 3000,
|
||||
};
|
||||
|
||||
export class ConfigStore {
|
||||
private filePath: string;
|
||||
private data: SavedSettings;
|
||||
|
||||
constructor(configPath?: string) {
|
||||
this.filePath = configPath || path.resolve('config.json');
|
||||
this.data = this.load();
|
||||
}
|
||||
|
||||
private load(): SavedSettings {
|
||||
if (!existsSync(this.filePath)) {
|
||||
logger.info({ path: this.filePath }, 'No config.json found, using defaults');
|
||||
return { ...DEFAULTS };
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = readFileSync(this.filePath, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as Partial<SavedSettings>;
|
||||
const merged = { ...DEFAULTS, ...parsed };
|
||||
// Migrate old links: add name/active fields, strip /live from URLs, default postAction
|
||||
merged.links = merged.links.map((l: any) => {
|
||||
const mode: LinkMode = l.mode || 'live';
|
||||
return {
|
||||
url: l.url.replace(/\/live\/?$/, ''),
|
||||
name: l.name || '',
|
||||
active: l.active !== undefined ? l.active : true,
|
||||
mode,
|
||||
postAction: l.postAction || (mode === 'scrap' ? 'salvage' : 'stash'),
|
||||
addedAt: l.addedAt || new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
logger.info({ path: this.filePath, linkCount: merged.links.length }, 'Loaded config.json');
|
||||
return merged;
|
||||
} catch (err) {
|
||||
logger.warn({ err, path: this.filePath }, 'Failed to read config.json, using defaults');
|
||||
return { ...DEFAULTS };
|
||||
}
|
||||
}
|
||||
|
||||
save(): void {
|
||||
try {
|
||||
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), 'utf-8');
|
||||
} catch (err) {
|
||||
logger.error({ err, path: this.filePath }, 'Failed to save config.json');
|
||||
}
|
||||
}
|
||||
|
||||
get settings(): SavedSettings {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
get links(): SavedLink[] {
|
||||
return this.data.links;
|
||||
}
|
||||
|
||||
addLink(url: string, name: string = '', mode: LinkMode = 'live', postAction?: PostAction): void {
|
||||
url = url.replace(/\/live\/?$/, '');
|
||||
if (this.data.links.some((l) => l.url === url)) return;
|
||||
this.data.links.push({
|
||||
url,
|
||||
name,
|
||||
active: true,
|
||||
mode,
|
||||
postAction: postAction || (mode === 'scrap' ? 'salvage' : 'stash'),
|
||||
addedAt: new Date().toISOString(),
|
||||
});
|
||||
this.save();
|
||||
}
|
||||
|
||||
removeLink(url: string): void {
|
||||
this.data.links = this.data.links.filter((l) => l.url !== url);
|
||||
this.save();
|
||||
}
|
||||
|
||||
removeLinkById(id: string): void {
|
||||
this.data.links = this.data.links.filter((l) => {
|
||||
const parts = l.url.split('/');
|
||||
return parts[parts.length - 1] !== id;
|
||||
});
|
||||
this.save();
|
||||
}
|
||||
|
||||
updateLinkById(id: string, updates: { name?: string; active?: boolean; mode?: LinkMode; postAction?: PostAction }): SavedLink | null {
|
||||
const link = this.data.links.find((l) => {
|
||||
const parts = l.url.split('/');
|
||||
return parts[parts.length - 1] === id;
|
||||
});
|
||||
if (!link) return null;
|
||||
if (updates.name !== undefined) link.name = updates.name;
|
||||
if (updates.active !== undefined) link.active = updates.active;
|
||||
if (updates.mode !== undefined) link.mode = updates.mode;
|
||||
if (updates.postAction !== undefined) link.postAction = updates.postAction;
|
||||
this.save();
|
||||
return link;
|
||||
}
|
||||
|
||||
setPaused(paused: boolean): void {
|
||||
this.data.paused = paused;
|
||||
this.save();
|
||||
}
|
||||
|
||||
updateSettings(partial: Record<string, unknown>): void {
|
||||
Object.assign(this.data, partial);
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
128
src-old/bot/LinkManager.ts
Normal file
128
src-old/bot/LinkManager.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { logger } from '../util/logger.js';
|
||||
import type { LinkMode, PostAction } from '../types.js';
|
||||
import type { ConfigStore } from './ConfigStore.js';
|
||||
|
||||
export interface TradeLink {
|
||||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
mode: LinkMode;
|
||||
postAction: PostAction;
|
||||
addedAt: string;
|
||||
}
|
||||
|
||||
export class LinkManager {
|
||||
private links: Map<string, TradeLink> = new Map();
|
||||
private store: ConfigStore;
|
||||
|
||||
constructor(store: ConfigStore) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
addLink(url: string, name: string = '', mode?: LinkMode, postAction?: PostAction): TradeLink {
|
||||
url = this.stripLive(url);
|
||||
const id = this.extractId(url);
|
||||
const label = this.extractLabel(url);
|
||||
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');
|
||||
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');
|
||||
}
|
||||
|
||||
toggleLink(id: string, active: boolean): TradeLink | undefined {
|
||||
const link = this.links.get(id);
|
||||
if (!link) return undefined;
|
||||
link.active = active;
|
||||
this.store.updateLinkById(id, { active });
|
||||
logger.info({ id, active }, `Trade link ${active ? 'activated' : 'deactivated'}`);
|
||||
return link;
|
||||
}
|
||||
|
||||
updateName(id: string, name: string): void {
|
||||
const link = this.links.get(id);
|
||||
if (!link) return;
|
||||
link.name = name;
|
||||
this.store.updateLinkById(id, { name });
|
||||
}
|
||||
|
||||
updateMode(id: string, mode: LinkMode): TradeLink | undefined {
|
||||
const link = this.links.get(id);
|
||||
if (!link) return undefined;
|
||||
link.mode = mode;
|
||||
this.store.updateLinkById(id, { mode });
|
||||
logger.info({ id, mode }, 'Trade link mode updated');
|
||||
return link;
|
||||
}
|
||||
|
||||
updatePostAction(id: string, postAction: PostAction): TradeLink | undefined {
|
||||
const link = this.links.get(id);
|
||||
if (!link) return undefined;
|
||||
link.postAction = postAction;
|
||||
this.store.updateLinkById(id, { postAction });
|
||||
logger.info({ id, postAction }, 'Trade link postAction updated');
|
||||
return link;
|
||||
}
|
||||
|
||||
isActive(id: string): boolean {
|
||||
const link = this.links.get(id);
|
||||
return link ? link.active : false;
|
||||
}
|
||||
|
||||
getLinks(): TradeLink[] {
|
||||
return Array.from(this.links.values());
|
||||
}
|
||||
|
||||
getLink(id: string): TradeLink | undefined {
|
||||
return this.links.get(id);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue