342 lines
9.9 KiB
TypeScript
342 lines
9.9 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
const scanCode = MapVirtualKeyW(vkCode, 0);
|
|
this.sendScanKeyDown(scanCode);
|
|
await randomDelay(15, 30);
|
|
}
|
|
|
|
async keyUp(vkCode: number): Promise<void> {
|
|
const scanCode = MapVirtualKeyW(vkCode, 0);
|
|
this.sendScanKeyUp(scanCode);
|
|
await randomDelay(15, 30);
|
|
}
|
|
|
|
async typeText(text: string): Promise<void> {
|
|
for (const char of text) {
|
|
this.sendUnicodeChar(char);
|
|
await randomDelay(20, 50);
|
|
}
|
|
}
|
|
|
|
async paste(): Promise<void> {
|
|
await this.keyDown(VK.CONTROL);
|
|
await sleep(30);
|
|
await this.pressKey(VK.V);
|
|
await this.keyUp(VK.CONTROL);
|
|
await sleep(50);
|
|
}
|
|
|
|
async selectAll(): Promise<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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);
|
|
}
|
|
}
|