import koffi from 'koffi'; import { sleep, randomDelay } from '../util/sleep.js'; // Win32 POINT struct for GetCursorPos const POINT = koffi.struct('POINT', { x: 'int32', y: 'int32' }); // Win32 INPUT struct on x64 is 40 bytes: // type (4) + pad (4) + union (32) // MOUSEINPUT is 32 bytes (the largest union member) // KEYBDINPUT is 24 bytes, so needs 8 bytes trailing pad in the union // // We define flat structs that match the exact memory layout, // then cast with koffi.as() when calling SendInput. const INPUT_KEYBOARD = koffi.struct('INPUT_KEYBOARD', { type: 'uint32', // offset 0 _pad0: 'uint32', // offset 4 (alignment for union at offset 8) wVk: 'uint16', // offset 8 wScan: 'uint16', // offset 10 dwFlags: 'uint32', // offset 12 time: 'uint32', // offset 16 _pad1: 'uint32', // offset 20 (alignment for dwExtraInfo) dwExtraInfo: 'uint64', // offset 24 _pad2: koffi.array('uint8', 8), // offset 32, pad to 40 bytes total }); const INPUT_MOUSE = koffi.struct('INPUT_MOUSE', { type: 'uint32', // offset 0 _pad0: 'uint32', // offset 4 (alignment for union at offset 8) dx: 'int32', // offset 8 dy: 'int32', // offset 12 mouseData: 'uint32', // offset 16 dwFlags: 'uint32', // offset 20 time: 'uint32', // offset 24 _pad1: 'uint32', // offset 28 (alignment for dwExtraInfo) dwExtraInfo: 'uint64', // offset 32 }); // INPUT_MOUSE is already 40 bytes, no trailing pad needed const user32 = koffi.load('user32.dll'); const SendInput = user32.func('SendInput', 'uint32', ['uint32', 'void *', 'int32']); const MapVirtualKeyW = user32.func('MapVirtualKeyW', 'uint32', ['uint32', 'uint32']); const GetSystemMetrics = user32.func('GetSystemMetrics', 'int32', ['int32']); const GetCursorPos = user32.func('GetCursorPos', 'int32', ['_Out_ POINT *']); // Constants const INPUT_MOUSE_TYPE = 0; const INPUT_KEYBOARD_TYPE = 1; const KEYEVENTF_SCANCODE = 0x0008; const KEYEVENTF_KEYUP = 0x0002; const KEYEVENTF_UNICODE = 0x0004; // Mouse flags const MOUSEEVENTF_MOVE = 0x0001; const MOUSEEVENTF_LEFTDOWN = 0x0002; const MOUSEEVENTF_LEFTUP = 0x0004; const MOUSEEVENTF_RIGHTDOWN = 0x0008; const MOUSEEVENTF_RIGHTUP = 0x0010; const MOUSEEVENTF_ABSOLUTE = 0x8000; // System metrics const SM_CXSCREEN = 0; const SM_CYSCREEN = 1; // Virtual key codes export const VK = { RETURN: 0x0d, CONTROL: 0x11, MENU: 0x12, // Alt SHIFT: 0x10, ESCAPE: 0x1b, TAB: 0x09, SPACE: 0x20, DELETE: 0x2e, BACK: 0x08, V: 0x56, A: 0x41, C: 0x43, I: 0x49, } as const; // Size to pass to SendInput (must be sizeof(INPUT) = 40 on x64) const INPUT_SIZE = koffi.sizeof(INPUT_MOUSE); // 40 // Bézier curve helpers for natural mouse movement function easeInOutQuad(t: number): number { return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2; } interface Point { x: number; y: number; } function cubicBezier(t: number, p0: Point, p1: Point, p2: Point, p3: Point): Point { const u = 1 - t; const u2 = u * u; const u3 = u2 * u; const t2 = t * t; const t3 = t2 * t; return { x: u3 * p0.x + 3 * u2 * t * p1.x + 3 * u * t2 * p2.x + t3 * p3.x, y: u3 * p0.y + 3 * u2 * t * p1.y + 3 * u * t2 * p2.y + t3 * p3.y, }; } function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); } export class InputSender { private screenWidth: number; private screenHeight: number; constructor() { this.screenWidth = GetSystemMetrics(SM_CXSCREEN); this.screenHeight = GetSystemMetrics(SM_CYSCREEN); } async pressKey(vkCode: number): Promise { const scanCode = MapVirtualKeyW(vkCode, 0); // MAPVK_VK_TO_VSC this.sendScanKeyDown(scanCode); await randomDelay(30, 50); this.sendScanKeyUp(scanCode); await randomDelay(20, 40); } async keyDown(vkCode: number): Promise { const scanCode = MapVirtualKeyW(vkCode, 0); this.sendScanKeyDown(scanCode); await randomDelay(15, 30); } async keyUp(vkCode: number): Promise { const scanCode = MapVirtualKeyW(vkCode, 0); this.sendScanKeyUp(scanCode); await randomDelay(15, 30); } async typeText(text: string): Promise { for (const char of text) { this.sendUnicodeChar(char); await randomDelay(20, 50); } } async paste(): Promise { await this.keyDown(VK.CONTROL); await sleep(30); await this.pressKey(VK.V); await this.keyUp(VK.CONTROL); await sleep(50); } async selectAll(): Promise { await this.keyDown(VK.CONTROL); await sleep(30); await this.pressKey(VK.A); await this.keyUp(VK.CONTROL); await sleep(50); } private getCursorPos(): Point { const pt = { x: 0, y: 0 }; GetCursorPos(pt); return pt; } private moveMouseRaw(x: number, y: number): void { const normalizedX = Math.round((x * 65535) / this.screenWidth); const normalizedY = Math.round((y * 65535) / this.screenHeight); this.sendMouseInput(normalizedX, normalizedY, 0, MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE); } async moveMouse(x: number, y: number): Promise { const start = this.getCursorPos(); const end: Point = { x, y }; const dx = end.x - start.x; const dy = end.y - start.y; const distance = Math.sqrt(dx * dx + dy * dy); // Short distance: just teleport if (distance < 10) { this.moveMouseRaw(x, y); await randomDelay(10, 20); return; } // Generate 2 random control points offset from the straight line const perpX = -dy / distance; const perpY = dx / distance; const spread = distance * 0.3; const cp1: Point = { x: start.x + dx * 0.25 + perpX * (Math.random() - 0.5) * spread, y: start.y + dy * 0.25 + perpY * (Math.random() - 0.5) * spread, }; const cp2: Point = { x: start.x + dx * 0.75 + perpX * (Math.random() - 0.5) * spread, y: start.y + dy * 0.75 + perpY * (Math.random() - 0.5) * spread, }; const steps = clamp(Math.round(distance / 30), 8, 20); for (let i = 1; i <= steps; i++) { const rawT = i / steps; const t = easeInOutQuad(rawT); const pt = cubicBezier(t, start, cp1, cp2, end); // Add ±1px jitter except on the last step const jitterX = i < steps ? Math.round((Math.random() - 0.5) * 2) : 0; const jitterY = i < steps ? Math.round((Math.random() - 0.5) * 2) : 0; this.moveMouseRaw(Math.round(pt.x) + jitterX, Math.round(pt.y) + jitterY); await sleep(1 + Math.random() * 2); // 1-3ms between steps } // Final exact landing this.moveMouseRaw(x, y); await randomDelay(5, 15); } moveMouseInstant(x: number, y: number): void { this.moveMouseRaw(x, y); } /** Quick Bézier move — ~10-15ms, 5 steps, no jitter. Fast but not a raw teleport. */ async moveMouseFast(x: number, y: number): Promise { const start = this.getCursorPos(); const end: Point = { x, y }; const dx = end.x - start.x; const dy = end.y - start.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 10) { this.moveMouseRaw(x, y); return; } const perpX = -dy / distance; const perpY = dx / distance; const spread = distance * 0.15; const cp1: Point = { x: start.x + dx * 0.3 + perpX * (Math.random() - 0.5) * spread, y: start.y + dy * 0.3 + perpY * (Math.random() - 0.5) * spread, }; const cp2: Point = { x: start.x + dx * 0.7 + perpX * (Math.random() - 0.5) * spread, y: start.y + dy * 0.7 + perpY * (Math.random() - 0.5) * spread, }; const steps = 5; for (let i = 1; i <= steps; i++) { const t = easeInOutQuad(i / steps); const pt = cubicBezier(t, start, cp1, cp2, end); this.moveMouseRaw(Math.round(pt.x), Math.round(pt.y)); await sleep(2); } this.moveMouseRaw(x, y); } async leftClick(x: number, y: number): Promise { await this.moveMouse(x, y); await randomDelay(20, 50); this.sendMouseInput(0, 0, 0, MOUSEEVENTF_LEFTDOWN); await randomDelay(15, 40); this.sendMouseInput(0, 0, 0, MOUSEEVENTF_LEFTUP); await randomDelay(15, 30); } async rightClick(x: number, y: number): Promise { await this.moveMouse(x, y); await randomDelay(20, 50); this.sendMouseInput(0, 0, 0, MOUSEEVENTF_RIGHTDOWN); await randomDelay(15, 40); this.sendMouseInput(0, 0, 0, MOUSEEVENTF_RIGHTUP); await randomDelay(15, 30); } async ctrlRightClick(x: number, y: number): Promise { await this.keyDown(VK.CONTROL); await randomDelay(30, 60); await this.rightClick(x, y); await this.keyUp(VK.CONTROL); await randomDelay(30, 60); } async ctrlLeftClick(x: number, y: number): Promise { await this.keyDown(VK.CONTROL); await randomDelay(30, 60); await this.leftClick(x, y); await this.keyUp(VK.CONTROL); await randomDelay(30, 60); } private sendMouseInput(dx: number, dy: number, mouseData: number, flags: number): void { const input = { type: INPUT_MOUSE_TYPE, _pad0: 0, dx, dy, mouseData, dwFlags: flags, time: 0, _pad1: 0, dwExtraInfo: 0, }; SendInput(1, koffi.as(input, 'INPUT_MOUSE *'), INPUT_SIZE); } private sendKeyInput(wVk: number, wScan: number, flags: number): void { const input = { type: INPUT_KEYBOARD_TYPE, _pad0: 0, wVk, wScan, dwFlags: flags, time: 0, _pad1: 0, dwExtraInfo: 0, _pad2: [0, 0, 0, 0, 0, 0, 0, 0], }; SendInput(1, koffi.as(input, 'INPUT_KEYBOARD *'), INPUT_SIZE); } private sendScanKeyDown(scanCode: number): void { this.sendKeyInput(0, scanCode, KEYEVENTF_SCANCODE); } private sendScanKeyUp(scanCode: number): void { this.sendKeyInput(0, scanCode, KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP); } private sendUnicodeChar(char: string): void { const code = char.charCodeAt(0); this.sendKeyInput(0, code, KEYEVENTF_UNICODE); this.sendKeyInput(0, code, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP); } }