From 72c549f20bdfbc4e2077da684917bcb6ae0f0827 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Tue, 11 Apr 2023 21:21:16 +0200 Subject: [PATCH 01/37] initial reworks --- _example_.py | 3 +- _example_mathpointer.py | 2 + _right_click.py | 36 ++++++--- interception.py | 4 +- src/interception/__init__.py | 2 + src/interception/_keycodes.py | 108 +++++++++++++++++++++++++++ src/interception/device.py | 124 +++++++++++++++++++++++++++++++ src/interception/interception.py | 117 +++++++++++++++++++++++++++++ src/interception/strokes.py | 98 ++++++++++++++++++++++++ test.py | 1 + 10 files changed, 482 insertions(+), 13 deletions(-) create mode 100644 src/interception/__init__.py create mode 100644 src/interception/_keycodes.py create mode 100644 src/interception/device.py create mode 100644 src/interception/interception.py create mode 100644 src/interception/strokes.py create mode 100644 test.py diff --git a/_example_.py b/_example_.py index ba7b3df..3c0d72f 100644 --- a/_example_.py +++ b/_example_.py @@ -3,9 +3,10 @@ if __name__ == "__main__": c = interception() - c.set_filter(interception.is_keyboard,interception_filter_key_state.INTERCEPTION_FILTER_KEY_UP.value) + c.set_filter(interception.is_mouse ,interception_filter_mouse_state.INTERCEPTION_FILTER_MOUSE_BUTTON_5_DOWN.value) while True: device = c.wait() + print(device) stroke = c.receive(device) if type(stroke) is key_stroke: print(stroke.code) diff --git a/_example_mathpointer.py b/_example_mathpointer.py index 0993278..6fbc8b9 100644 --- a/_example_mathpointer.py +++ b/_example_mathpointer.py @@ -113,6 +113,8 @@ def math_track(context:interception, mouse : int, partitioning): delta = t2 - t1 position = curve(t1) + print(center.x, center.y) + mstroke = mouse_stroke(interception_mouse_state.INTERCEPTION_MOUSE_LEFT_BUTTON_UP.value, interception_mouse_flag.INTERCEPTION_MOUSE_MOVE_ABSOLUTE.value, 0, diff --git a/_right_click.py b/_right_click.py index 8987355..413ca67 100644 --- a/_right_click.py +++ b/_right_click.py @@ -1,3 +1,6 @@ +import time + +import pyautogui from interception import * from win32api import GetSystemMetrics @@ -17,21 +20,34 @@ break # no mouse we quit -if (mouse == 0): +if mouse == 0: print("No mouse found") exit(0) +_screen_width = GetSystemMetrics(0) +_screen_height = GetSystemMetrics(1) + +def to_hexadecimal(screen_size: int, i: int): + return int((0xFFFF / screen_size) * i) +x, y = 1439,230 # we create a new mouse stroke, initially we use set right button down, we also use absolute move, # and for the coordinate (x and y) we use center screen -mstroke = mouse_stroke(interception_mouse_state.INTERCEPTION_MOUSE_RIGHT_BUTTON_DOWN.value, - interception_mouse_flag.INTERCEPTION_MOUSE_MOVE_ABSOLUTE.value, - 0, - int((0xFFFF * screen_width/2) / screen_width), - int((0xFFFF * screen_height/2) / screen_height), - 0) +mstroke = mouse_stroke( + 0, + interception_mouse_flag.INTERCEPTION_MOUSE_MOVE_ABSOLUTE.value, + 0, + to_hexadecimal(_screen_width, x), + to_hexadecimal(_screen_height, y), + 0, +) + +context.send(13, mstroke) # we send the key stroke, now the right button is down + +time.sleep(1) -context.send(mouse,mstroke) # we send the key stroke, now the right button is down -mstroke.state = interception_mouse_state.INTERCEPTION_MOUSE_RIGHT_BUTTON_UP.value # update the stroke to release the button -context.send(mouse,mstroke) #button right is up \ No newline at end of file +mstroke.state = ( + interception_mouse_state.INTERCEPTION_MOUSE_RIGHT_BUTTON_UP.value +) # update the stroke to release the button +context.send(15, mstroke) # button right is up diff --git a/interception.py b/interception.py index 0209fb2..ce16cec 100644 --- a/interception.py +++ b/interception.py @@ -8,8 +8,8 @@ k32 = windll.LoadLibrary('kernel32') -class interception(): - _context = [] +class interception: + _context: list = [] k32 = None _c_events = (c_void_p * MAX_DEVICES)() diff --git a/src/interception/__init__.py b/src/interception/__init__.py new file mode 100644 index 0000000..18035be --- /dev/null +++ b/src/interception/__init__.py @@ -0,0 +1,2 @@ +from .interception import Interception +from .device import Device \ No newline at end of file diff --git a/src/interception/_keycodes.py b/src/interception/_keycodes.py new file mode 100644 index 0000000..f6ea7a7 --- /dev/null +++ b/src/interception/_keycodes.py @@ -0,0 +1,108 @@ +import ctypes + +KEYBOARD_MAPPING = { + 'escape': 0x01, + 'esc': 0x01, + 'f1': 0x3B, + 'f2': 0x3C, + 'f3': 0x3D, + 'f4': 0x3E, + 'f5': 0x3F, + 'f6': 0x40, + 'f7': 0x41, + 'f8': 0x42, + 'f9': 0x43, + 'f10': 0x44, + 'f11': 0x57, + 'f12': 0x58, + 'printscreen': 0xB7, + 'prntscrn': 0xB7, + 'prtsc': 0xB7, + 'prtscr': 0xB7, + 'scrolllock': 0x46, + 'pause': 0xC5, + '`': 0x29, + '1': 0x02, + '2': 0x03, + '3': 0x04, + '4': 0x05, + '5': 0x06, + '6': 0x07, + '7': 0x08, + '8': 0x09, + '9': 0x0A, + '0': 0x0B, + '-': 0x0C, + '=': 0x0D, + 'backspace': 0x0E, + 'insert': 0xD2 + 1024, + 'home': 0xC7 + 1024, + 'pageup': 0xC9 + 1024, + 'pagedown': 0xD1 + 1024, + 'numlock': 0x45, + 'divide': 0xB5 + 1024, + 'multiply': 0x37, + 'subtract': 0x4A, + 'add': 0x4E, + 'decimal': 0x53, + 'tab': 0x0F, + 'q': 0x10, + 'w': 0x11, + 'e': 0x12, + 'r': 0x13, + 't': 0x14, + 'y': 0x15, + 'u': 0x16, + 'i': 0x17, + 'o': 0x18, + 'p': 0x19, + '[': 0x1A, + ']': 0x1B, + '\\': 0x2B, + 'del': 0xD3 + 1024, + 'delete': 0xD3 + 1024, + 'end': 0xCF + 1024, + 'capslock': 0x3A, + 'a': 0x1E, + 's': 0x1F, + 'd': 0x20, + 'f': 0x21, + 'g': 0x22, + 'h': 0x23, + 'j': 0x24, + 'k': 0x25, + 'l': 0x26, + ';': 0x27, + "'": 0x28, + 'enter': 0x1C, + 'return': 0x1C, + 'shift': 0x2A, + 'shiftleft': 0x2A, + 'z': 0x2C, + 'x': 0x2D, + 'c': 0x2E, + 'v': 0x2F, + 'b': 0x30, + 'n': 0x31, + 'm': 0x32, + ',': 0x33, + '.': 0x34, + '/': 0x35, + 'shiftright': 0x36, + 'ctrl': 0x1D, + 'ctrlleft': 0x1D, + 'win': 0xDB + 1024, + 'winleft': 0xDB + 1024, + 'alt': 0x38, + 'altleft': 0x38, + ' ': 0x39, + 'space': 0x39, + 'altright': 0xB8 + 1024, + 'winright': 0xDC + 1024, + 'apps': 0xDD + 1024, + 'ctrlright': 0x9D + 1024, + 'up': ctypes.windll.user32.MapVirtualKeyW(0x26, 0), + 'left': ctypes.windll.user32.MapVirtualKeyW(0x25, 0), + 'down': ctypes.windll.user32.MapVirtualKeyW(0x28, 0), + 'right': ctypes.windll.user32.MapVirtualKeyW(0x27, 0), +} \ No newline at end of file diff --git a/src/interception/device.py b/src/interception/device.py new file mode 100644 index 0000000..4d4e7ac --- /dev/null +++ b/src/interception/device.py @@ -0,0 +1,124 @@ +from ctypes import c_byte, c_int, c_ushort, memmove, windll +from dataclasses import dataclass, field +from typing import Any, Type + +from .strokes import KeyStroke, MouseStroke, Stroke + +k32 = windll.LoadLibrary("kernel32") + + +def device_io_call(decorated): + def decorator(device: Device, *args, **kwargs): + command, inbuffer, outbuffer = decorated(device, *args, **kwargs) + return device._device_io_control(command, inbuffer, outbuffer) + + return decorator + + +@dataclass +class DeviceIOResult: + result: int + data: Any + data_bytes: bytes = field(init=False) + + def __post_init__(self): + if self.data is not None: + self.data = list(self.data) + self.data_bytes = bytes(self.data) + + +class Device: + _bytes_returned = (c_int * 1)(0) + _c_byte_500 = (c_byte * 500)() + _c_int_2 = (c_int * 2)() + _c_ushort_1 = (c_ushort * 1)() + _c_int_1 = (c_int * 1)() + + def __init__(self, handle, event, *, is_keyboard: bool): + self.is_keyboard = is_keyboard + self._parser: Type[KeyStroke] | Type[MouseStroke] + if is_keyboard: + self._c_recv_buffer = (c_byte * 12)() + self._parser = KeyStroke + else: + self._c_recv_buffer = (c_byte * 24)() + self._parser = MouseStroke + + if handle == -1 or event == 0: + raise Exception("Can't create device!") + + self.handle = handle + self.event = event + + if self._device_set_event().result == 0: + raise Exception("Can't communicate with driver") + + def destroy(self): + if self.handle != -1: + k32.CloseHandle(self.handle) + + if self.event: + k32.CloseHandle(self.event) + + @device_io_call + def get_precedence(self): + return 0x222008, 0, self._c_int_1 + + @device_io_call + def set_precedence(self, precedence: int): + self._c_int_1[0] = precedence + return 0x222004, self._c_int_1, 0 + + @device_io_call + def get_filter(self): + return 0x222020, 0, self._c_ushort_1 + + @device_io_call + def set_filter(self, filter): + self._c_ushort_1[0] = filter + return 0x222010, self._c_ushort_1, 0 + + @device_io_call + def _get_HWID(self): + return 0x222200, 0, self._c_byte_500 + + def get_HWID(self): + data = self._get_HWID().data_bytes + return data[: self._bytes_returned[0]] + + @device_io_call + def _receive(self): + return 0x222100, 0, self._c_recv_buffer + + def receive(self): + data = self._receive().data_bytes + return self._parser.parse_raw(data) + + def send(self, stroke: Stroke): + if not isinstance(stroke, self._parser): + raise ValueError(f"Can't parse {stroke} with {type(self._parser)}!") + self._send(stroke) + + @device_io_call + def _send(self, stroke: Stroke): + memmove(self._c_recv_buffer, stroke.raw_data, len(self._c_recv_buffer)) + return 0x222080, self._c_recv_buffer, 0 + + @device_io_call + def _device_set_event(self): + self._c_int_2[0] = self.event + return 0x222040, self._c_int_2, 0 + + def _device_io_control(self, command, inbuffer, outbuffer) -> DeviceIOResult: + res = k32.DeviceIoControl( + self.handle, + command, + inbuffer, + len(bytes(inbuffer)) if inbuffer else 0, + outbuffer, + len(bytes(outbuffer)) if outbuffer else 0, + self._bytes_returned, + 0, + ) + + return DeviceIOResult(res, outbuffer if outbuffer else None) diff --git a/src/interception/interception.py b/src/interception/interception.py new file mode 100644 index 0000000..b674cbd --- /dev/null +++ b/src/interception/interception.py @@ -0,0 +1,117 @@ +from ctypes import * +from ctypes import _NamedFuncPointer +from typing import Final + +from consts import * + + +MAX_DEVICES: Final = 20 +MAX_KEYBOARD: Final = 10 +MAX_MOUSE: Final = 10 + +from .device import Device +from .strokes import Stroke + +k32 = windll.LoadLibrary("kernel32") + + +class Interception: + _context: list[Device] = [] + _c_events: Array[c_void_p] = (c_void_p * MAX_DEVICES)() + + def __init__(self): + try: + self.build_handles() + except IOError as e: + self._destroy_context() + raise e + + def build_handles(self) -> None: + """Creates handles and events for all interception devices. + + Iterates over all interception devices and creates a `Device` object for each one. + A `Device` object represents an interception device and includes a handle to the device, + an event that can be used to wait for input on the device, and a flag indicating whether + the device is a keyboard or a mouse. + + The handle is created using the `create_device_handle()` method, which calls the Windows API + function `CreateFileA()` with the appropriate parameters. + + The event is created using the Windows API function `CreateEventA()`, which creates a + synchronization event that can be signaled when input is available on the device. + + The `is_keyboard()` method is called to determine whether the device is a keyboard or a mouse. + This is used to set the `is_keyboard` flag on the Device object. + + The created Device objects are added to the context list and the corresponding event + handle is added to the c_events dictionary. + + Raises: + IOError: If a device handle cannot be created. + """ + for device_num in range(MAX_DEVICES): + device = Device( + self.create_device_handle(device_num), + k32.CreateEventA(0, 1, 0, 0), + is_keyboard=self.is_keyboard(device_num), + ) + self._context.append(device) + self._c_events[device_num] = device.event + + def wait(self, milliseconds: int = -1): + result = k32.WaitForMultipleObjects( + MAX_DEVICES, self._c_events, 0, milliseconds + ) + if result in [-1, 0x102]: + return 0 + return result + + def get_HWID(self, device: int): + if self.is_invalid(device): + return "" + try: + return self._context[device].get_HWID().decode("utf-16") + except: + return "" + + def receive(self, device: int): + if not self.is_invalid(device): + return self._context[device].receive() + + def send(self, device: int, stroke: Stroke): + if not self.is_invalid(device): + self._context[device].send(stroke) + + @staticmethod + def is_keyboard(device): + return device + 1 > 0 and device + 1 <= MAX_KEYBOARD + + @staticmethod + def is_mouse(device): + return device + 1 > MAX_KEYBOARD and device + 1 <= MAX_KEYBOARD + MAX_MOUSE + + @staticmethod + def is_invalid(device): + return device + 1 <= 0 or device + 1 > (MAX_KEYBOARD + MAX_MOUSE) + + @staticmethod + def create_device_handle(device_num: int) -> _NamedFuncPointer: + """Creates a handle to a specified device. + + Access mode for the device is `GENERIC_READ | GENERIC_WRITE`, allows the + handle to read and write to the device. + + Sharing mode for the device is `FILE_SHARE_READ | FILE_SHARE_WRITE`, which + allows other processes to read from and write to the device while it is open. + + Creation disposition for the device is `OPEN_EXISTING`, indicating that the device + should be opened if it already exists. + + Flags and attributes for the device are not used in this case. + """ + device_name = f"\\\\.\\interception{device_num:02d}".encode() + return k32.CreateFileA(device_name, 0x80000000, 0, 0, 3, 0, 0) + + def _destroy_context(self): + for device in self._context: + device.destroy() diff --git a/src/interception/strokes.py b/src/interception/strokes.py new file mode 100644 index 0000000..dad3be6 --- /dev/null +++ b/src/interception/strokes.py @@ -0,0 +1,98 @@ +import struct +from typing import Protocol + + +class Stroke(Protocol): + + @classmethod + def parse(cls, self): + ... + + @classmethod + def parse_raw(cls, self): + ... + + @property + def data(self): + ... + + @property + def raw_data(self): + ... + + +class MouseStroke: + fmt = "HHhiiI" + fmt_raw = "HHHHIiiI" + + def __init__(self, state, flags, rolling, x, y, information): + self.state = state + self.flags = flags + self.rolling = rolling + self.x = x + self.y = y + self.information = information + + @classmethod + def parse(cls, data): + return cls(*struct.unpack(cls.fmt, data)) + + @classmethod + def parse_raw(cls, data): + unpacked = struct.unpack(cls.fmt_raw, data) + return cls(**(unpacked[i] for i in (2, 1, 3, 5, 6, 7))) + + @property + def data(self) -> bytes: + return struct.pack( + self.fmt, + self.state, + self.flags, + self.rolling, + self.x, + self.y, + self.information, + ) + + @property + def raw_data(self) -> bytes: + return struct.pack( + self.fmt_raw, + 0, + self.flags, + self.state, + self.rolling, + 0, + self.x, + self.y, + self.information, + ) + + +class KeyStroke: + fmt = "HHI" + fmt_raw = "HHHHI" + + def __init__(self, code, state, information): + self.code = code + self.state = state + self.information = information + + @classmethod + def parse(cls, data): + return cls(*struct.unpack(cls.fmt, data)) + + @classmethod + def parse_raw(cls, data): + unpacked = struct.unpack(cls.fmt_raw, data) + return cls(unpacked[1], unpacked[2], unpacked[4]) + + @property + def data(self): + data = struct.pack(self.fmt, self.code, self.state, self.information) + return data + + @property + def raw_data(self): + data = struct.pack(self.fmt_raw, 0, self.code, self.state, 0, self.information) + return data diff --git a/test.py b/test.py new file mode 100644 index 0000000..70dba21 --- /dev/null +++ b/test.py @@ -0,0 +1 @@ +from src.interception import inter \ No newline at end of file From ccf46b2e09be081ebf5b2c71e9fb66b0ef23edae Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Tue, 11 Apr 2023 21:53:08 +0200 Subject: [PATCH 02/37] added constants --- .gitignore | 1 + _example_.py | 16 -- _example_hardwareid.py | 17 -- _example_mathpointer.py | 258 ------------------------------- _right_click.py | 53 ------- consts.py | 79 ---------- interception.py | 182 ---------------------- src/interception/__init__.py | 4 +- src/interception/consts.py | 65 ++++++++ src/interception/device.py | 5 +- src/interception/interception.py | 20 ++- src/interception/strokes.py | 38 +++-- stroke.py | 105 ------------- 13 files changed, 111 insertions(+), 732 deletions(-) delete mode 100644 _example_.py delete mode 100644 _example_hardwareid.py delete mode 100644 _example_mathpointer.py delete mode 100644 _right_click.py delete mode 100644 consts.py delete mode 100644 interception.py create mode 100644 src/interception/consts.py delete mode 100644 stroke.py diff --git a/.gitignore b/.gitignore index 3e759b7..747cd1d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.user *.userosscache *.sln.docstates +test.py # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/_example_.py b/_example_.py deleted file mode 100644 index 3c0d72f..0000000 --- a/_example_.py +++ /dev/null @@ -1,16 +0,0 @@ -from interception import * -from consts import * - -if __name__ == "__main__": - c = interception() - c.set_filter(interception.is_mouse ,interception_filter_mouse_state.INTERCEPTION_FILTER_MOUSE_BUTTON_5_DOWN.value) - while True: - device = c.wait() - print(device) - stroke = c.receive(device) - if type(stroke) is key_stroke: - print(stroke.code) - c.send(device,stroke) - # hwid = c.get_HWID(device) - # print(u"%s" % hwid) - \ No newline at end of file diff --git a/_example_hardwareid.py b/_example_hardwareid.py deleted file mode 100644 index 4b14750..0000000 --- a/_example_hardwareid.py +++ /dev/null @@ -1,17 +0,0 @@ -from interception import * -from consts import * - -SCANCODE_ESC = 0x01 - -if __name__ == "__main__": - c = interception() - c.set_filter(interception.is_keyboard,interception_filter_key_state.INTERCEPTION_FILTER_KEY_UP.value | interception_filter_key_state.INTERCEPTION_FILTER_KEY_DOWN.value) - c.set_filter(interception.is_mouse,interception_filter_mouse_state.INTERCEPTION_FILTER_MOUSE_LEFT_BUTTON_DOWN.value) - while True: - device = c.wait() - stroke = c.receive(device) - c.send(device,stroke) - if stroke is None or (interception.is_keyboard(device) and stroke.code == SCANCODE_ESC): - break - print(c.get_HWID(device)) - c._destroy_context() diff --git a/_example_mathpointer.py b/_example_mathpointer.py deleted file mode 100644 index 6fbc8b9..0000000 --- a/_example_mathpointer.py +++ /dev/null @@ -1,258 +0,0 @@ -from interception import * -from consts import * -from math import * -from win32api import GetSystemMetrics -from datetime import datetime -from time import sleep - -esc = 0x01 -num_0 = 0x0B -num_1 = 0x02 -num_2 = 0x03 -num_3 = 0x04 -num_4 = 0x05 -num_5 = 0x06 -num_6 = 0x07 -num_7 = 0x08 -num_8 = 0x09 -num_9 = 0x0A -scale = 15 -screen_width = GetSystemMetrics(0) -screen_height = GetSystemMetrics(1) - -def delay(): - sleep(0.001) - -class point(): - x = 0 - y = 0 - def __init__(self,x,y): - self.x = x - self.y = y - -def circle(t): - f = 10 - return point(scale * f * cos(t), scale * f *sin(t)) - -def mirabilis(t): - f= 1 / 2 - k = 1 / (2 * pi) - - return point(scale * f * (exp(k * t) * cos(t)), - scale * f * (exp(k * t) * sin(t))) - -def epitrochoid(t): - f = 1 - R = 6 - r = 2 - d = 1 - c = R + r - - return point(scale * f * (c * cos(t) - d * cos((c * t) / r)), - scale * f * (c * sin(t) - d * sin((c * t) / r))) - -def hypotrochoid(t): - f = 10 / 7 - R = 5 - r = 3 - d = 5 - c = R - r - - return point(scale * f * (c * cos(t) + d * cos((c * t) / r)), - scale * f * (c * sin(t) - d * sin((c * t) / r))) - -def hypocycloid(t): - f = 10 / 3 - R = 3 - r = 1 - c = R - r - - return point(scale * f * (c * cos(t) + r * cos((c * t) / r)), - scale * f * (c * sin(t) - r * sin((c * t) / r))) - -def bean(t): - f = 10 - c = cos(t) - s = sin(t) - - return point(scale * f * ((pow(c, 3) + pow(s, 3)) * c), - scale * f * ((pow(c, 3) + pow(s, 3)) * s)) - -def Lissajous(t): - f = 10 - a = 2 - b = 3 - - return point(scale * f * (sin(a * t)), scale * f * (sin(b * t))) - -def epicycloid(t): - f = 10 / 42 - R = 21 - r = 10 - c = R + r - - return point(scale * f * (c * cos(t) - r * cos((c * t) / r)), - scale * f * (c * sin(t) - r * sin((c * t) / r))) - -def rose(t): - f = 10 - R = 1 - k = 2 / 7 - - return point(scale * f * (R * cos(k * t) * cos(t)), - scale * f * (R * cos(k * t) * sin(t))) - -def butterfly(t): - f = 10 / 4 - c = exp(cos(t)) - 2 * cos(4 * t) + pow(sin(t / 12), 5) - - return point(scale * f * (sin(t) * c), scale * f * (cos(t) * c)) - -def math_track(context:interception, mouse : int, - center,curve, t1, t2, # changed params order - partitioning): - delta = t2 - t1 - position = curve(t1) - print(center.x, center.y) - - mstroke = mouse_stroke(interception_mouse_state.INTERCEPTION_MOUSE_LEFT_BUTTON_UP.value, - interception_mouse_flag.INTERCEPTION_MOUSE_MOVE_ABSOLUTE.value, - 0, - int((0xFFFF * center.x) / screen_width), - int((0xFFFF * center.y) / screen_height), - 0) - - context.send(mouse,mstroke) - - mstroke.state = 0 - mstroke.x = int((0xFFFF * (center.x + position.x)) / screen_width) - mstroke.y = int((0xFFFF * (center.y - position.y)) / screen_height) - - context.send(mouse,mstroke) - - j = 0 - for i in range(partitioning+2): - if (j % 250 == 0): - delay() - mstroke.state = interception_mouse_state.INTERCEPTION_MOUSE_LEFT_BUTTON_UP.value - context.send(mouse,mstroke) - - delay() - mstroke.state = interception_mouse_state.INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN.value - context.send(mouse,mstroke) - if i > 0: - i = i-2 - - position = curve(t1 + (i * delta)/partitioning) - mstroke.x = int((0xFFFF * (center.x + position.x)) / screen_width) - mstroke.y = int((0xFFFF * (center.y - position.y)) / screen_height) - context.send(mouse,mstroke) - delay() - j = j + 1 - - delay() - mstroke.state = interception_mouse_state.INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN.value - context.send(mouse,mstroke) - - delay() - mstroke.state = interception_mouse_state.INTERCEPTION_MOUSE_LEFT_BUTTON_UP.value - context.send(mouse,mstroke) - - delay() - mstroke.state = 0 - mstroke.x = int((0xFFFF * center.x) / screen_width) - mstroke.y = int((0xFFFF * center.y) / screen_height) - context.send(mouse,mstroke) - -curves = { num_0 : (circle,0,2*pi,200), - num_1 : (mirabilis,-6*pi,6*pi,200), - num_2 : (epitrochoid,0, 2 * pi, 200), - num_3 : (hypotrochoid, 0, 6 * pi, 200), - num_4 : (hypocycloid,0, 2 * pi, 200), - num_5 : (bean, 0, pi, 200), - num_6 : (Lissajous, 0, 2 * pi, 200), - num_7 : (epicycloid, 0, 20 * pi, 1000), - num_8 : (rose,0, 14 * pi, 500), - num_9 : (butterfly, 0, 21 * pi, 2000), - } - - -notice = '''NOTICE: This example works on real machines. -Virtual machines generally work with absolute mouse -positioning over the screen, which this samples isn't\n" -prepared to handle. - -Now please, first move the mouse that's going to be impersonated. -''' - -steps = '''Impersonating mouse %d -Now: - - Go to Paint (or whatever place you want to draw) - - Select your pencil - - Position your mouse in the drawing board - - Press any digit (not numpad) on your keyboard to draw an equation - - Press ESC to exit.''' - -def main(): - - mouse = 0 - position = point(screen_width // 2, screen_height // 2) - context = interception() - context.set_filter(interception.is_keyboard, - interception_filter_key_state.INTERCEPTION_FILTER_KEY_DOWN.value | - interception_filter_key_state.INTERCEPTION_FILTER_KEY_UP.value) - context.set_filter(interception.is_mouse, - interception_filter_mouse_state.INTERCEPTION_FILTER_MOUSE_MOVE.value ) - - print(notice) - - while True: - - device = context.wait() - if interception.is_mouse(device): - if mouse == 0: - mouse = device - print( steps % (device - 10)) - - mstroke = context.receive(device) - - position.x += mstroke.x - position.y += mstroke.y - - if position.x < 0: - position.x = 0 - if position.x > screen_width - 1: - position.x = screen_width -1 - - if position.y <0 : - position.y = 0 - if position.y > screen_height - 1: - position.y = screen_height -1 - - mstroke.flags = interception_mouse_flag.INTERCEPTION_MOUSE_MOVE_ABSOLUTE.value - mstroke.x = int((0xFFFF * position.x) / screen_width) - mstroke.y = int((0xFFFF * position.y) / screen_height) - - context.send(device,mstroke) - - if mouse and interception.is_keyboard(device): - kstroke = context.receive(device) - - if kstroke.code == esc: - return - - if kstroke.state == interception_key_state.INTERCEPTION_KEY_DOWN.value: - if kstroke.code in curves: - math_track(context,mouse,position,*curves[kstroke.code]) - else: - context.send(device,kstroke) - - elif kstroke.state == interception_key_state.INTERCEPTION_KEY_UP.value: - if not kstroke.code in curves: - context.send(device,kstroke) - else: - context.send(device,kstroke) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/_right_click.py b/_right_click.py deleted file mode 100644 index 413ca67..0000000 --- a/_right_click.py +++ /dev/null @@ -1,53 +0,0 @@ -import time - -import pyautogui -from interception import * -from win32api import GetSystemMetrics - -# get screen size -screen_width = GetSystemMetrics(0) -screen_height = GetSystemMetrics(1) - -# create a context for interception to use to send strokes, in this case -# we won't use filters, we will manually search for the first found mouse -context = interception() - -# loop through all devices and check if they correspond to a mouse -mouse = 0 -for i in range(MAX_DEVICES): - if interception.is_mouse(i): - mouse = i - break - -# no mouse we quit -if mouse == 0: - print("No mouse found") - exit(0) - -_screen_width = GetSystemMetrics(0) -_screen_height = GetSystemMetrics(1) - -def to_hexadecimal(screen_size: int, i: int): - return int((0xFFFF / screen_size) * i) - -x, y = 1439,230 -# we create a new mouse stroke, initially we use set right button down, we also use absolute move, -# and for the coordinate (x and y) we use center screen -mstroke = mouse_stroke( - 0, - interception_mouse_flag.INTERCEPTION_MOUSE_MOVE_ABSOLUTE.value, - 0, - to_hexadecimal(_screen_width, x), - to_hexadecimal(_screen_height, y), - 0, -) - -context.send(13, mstroke) # we send the key stroke, now the right button is down - -time.sleep(1) - - -mstroke.state = ( - interception_mouse_state.INTERCEPTION_MOUSE_RIGHT_BUTTON_UP.value -) # update the stroke to release the button -context.send(15, mstroke) # button right is up diff --git a/consts.py b/consts.py deleted file mode 100644 index 5ad70b8..0000000 --- a/consts.py +++ /dev/null @@ -1,79 +0,0 @@ -from enum import Enum - -class interception_key_state(Enum): - INTERCEPTION_KEY_DOWN = 0x00 - INTERCEPTION_KEY_UP = 0x01 - INTERCEPTION_KEY_E0 = 0x02 - INTERCEPTION_KEY_E1 = 0x04 - INTERCEPTION_KEY_TERMSRV_SET_LED = 0x08 - INTERCEPTION_KEY_TERMSRV_SHADOW = 0x10 - INTERCEPTION_KEY_TERMSRV_VKPACKET = 0x20 - -class interception_filter_key_state(Enum): - INTERCEPTION_FILTER_KEY_NONE = 0x0000 - INTERCEPTION_FILTER_KEY_ALL = 0xFFFF - INTERCEPTION_FILTER_KEY_DOWN = interception_key_state.INTERCEPTION_KEY_UP.value - INTERCEPTION_FILTER_KEY_UP = interception_key_state.INTERCEPTION_KEY_UP.value << 1 - INTERCEPTION_FILTER_KEY_E0 = interception_key_state.INTERCEPTION_KEY_E0.value << 1 - INTERCEPTION_FILTER_KEY_E1 = interception_key_state.INTERCEPTION_KEY_E1.value << 1 - INTERCEPTION_FILTER_KEY_TERMSRV_SET_LED = interception_key_state.INTERCEPTION_KEY_TERMSRV_SET_LED.value << 1 - INTERCEPTION_FILTER_KEY_TERMSRV_SHADOW = interception_key_state.INTERCEPTION_KEY_TERMSRV_SHADOW.value << 1 - INTERCEPTION_FILTER_KEY_TERMSRV_VKPACKET = interception_key_state.INTERCEPTION_KEY_TERMSRV_VKPACKET.value << 1 - -class interception_mouse_state (Enum): - INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN = 0x001 - INTERCEPTION_MOUSE_LEFT_BUTTON_UP = 0x002 - INTERCEPTION_MOUSE_RIGHT_BUTTON_DOWN = 0x004 - INTERCEPTION_MOUSE_RIGHT_BUTTON_UP = 0x008 - INTERCEPTION_MOUSE_MIDDLE_BUTTON_DOWN = 0x010 - INTERCEPTION_MOUSE_MIDDLE_BUTTON_UP = 0x020 - - INTERCEPTION_MOUSE_BUTTON_1_DOWN = INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN - INTERCEPTION_MOUSE_BUTTON_1_UP = INTERCEPTION_MOUSE_LEFT_BUTTON_UP - INTERCEPTION_MOUSE_BUTTON_2_DOWN = INTERCEPTION_MOUSE_RIGHT_BUTTON_DOWN - INTERCEPTION_MOUSE_BUTTON_2_UP = INTERCEPTION_MOUSE_RIGHT_BUTTON_UP - INTERCEPTION_MOUSE_BUTTON_3_DOWN = INTERCEPTION_MOUSE_MIDDLE_BUTTON_DOWN - INTERCEPTION_MOUSE_BUTTON_3_UP = INTERCEPTION_MOUSE_MIDDLE_BUTTON_UP - - INTERCEPTION_MOUSE_BUTTON_4_DOWN = 0x040 - INTERCEPTION_MOUSE_BUTTON_4_UP = 0x080 - INTERCEPTION_MOUSE_BUTTON_5_DOWN = 0x100 - INTERCEPTION_MOUSE_BUTTON_5_UP = 0x200 - - INTERCEPTION_MOUSE_WHEEL = 0x400 - INTERCEPTION_MOUSE_HWHEEL = 0x800 - -class interception_filter_mouse_state(Enum): - INTERCEPTION_FILTER_MOUSE_NONE = 0x0000 - INTERCEPTION_FILTER_MOUSE_ALL = 0xFFFF - - INTERCEPTION_FILTER_MOUSE_LEFT_BUTTON_DOWN = interception_mouse_state.INTERCEPTION_MOUSE_LEFT_BUTTON_DOWN.value - INTERCEPTION_FILTER_MOUSE_LEFT_BUTTON_UP = interception_mouse_state.INTERCEPTION_MOUSE_LEFT_BUTTON_UP.value - INTERCEPTION_FILTER_MOUSE_RIGHT_BUTTON_DOWN = interception_mouse_state.INTERCEPTION_MOUSE_RIGHT_BUTTON_DOWN.value - INTERCEPTION_FILTER_MOUSE_RIGHT_BUTTON_UP = interception_mouse_state.INTERCEPTION_MOUSE_RIGHT_BUTTON_UP.value - INTERCEPTION_FILTER_MOUSE_MIDDLE_BUTTON_DOWN = interception_mouse_state.INTERCEPTION_MOUSE_MIDDLE_BUTTON_DOWN.value - INTERCEPTION_FILTER_MOUSE_MIDDLE_BUTTON_UP = interception_mouse_state.INTERCEPTION_MOUSE_MIDDLE_BUTTON_UP.value - - INTERCEPTION_FILTER_MOUSE_BUTTON_1_DOWN = interception_mouse_state.INTERCEPTION_MOUSE_BUTTON_1_DOWN.value - INTERCEPTION_FILTER_MOUSE_BUTTON_1_UP = interception_mouse_state.INTERCEPTION_MOUSE_BUTTON_1_UP.value - INTERCEPTION_FILTER_MOUSE_BUTTON_2_DOWN = interception_mouse_state.INTERCEPTION_MOUSE_BUTTON_2_DOWN.value - INTERCEPTION_FILTER_MOUSE_BUTTON_2_UP = interception_mouse_state.INTERCEPTION_MOUSE_BUTTON_2_UP.value - INTERCEPTION_FILTER_MOUSE_BUTTON_3_DOWN = interception_mouse_state.INTERCEPTION_MOUSE_BUTTON_3_DOWN.value - INTERCEPTION_FILTER_MOUSE_BUTTON_3_UP = interception_mouse_state.INTERCEPTION_MOUSE_BUTTON_3_UP.value - - INTERCEPTION_FILTER_MOUSE_BUTTON_4_DOWN = interception_mouse_state.INTERCEPTION_MOUSE_BUTTON_4_DOWN.value - INTERCEPTION_FILTER_MOUSE_BUTTON_4_UP = interception_mouse_state.INTERCEPTION_MOUSE_BUTTON_4_UP.value - INTERCEPTION_FILTER_MOUSE_BUTTON_5_DOWN = interception_mouse_state.INTERCEPTION_MOUSE_BUTTON_5_DOWN.value - INTERCEPTION_FILTER_MOUSE_BUTTON_5_UP = interception_mouse_state.INTERCEPTION_MOUSE_BUTTON_5_UP.value - - INTERCEPTION_FILTER_MOUSE_WHEEL = interception_mouse_state.INTERCEPTION_MOUSE_WHEEL.value - INTERCEPTION_FILTER_MOUSE_HWHEEL = interception_mouse_state.INTERCEPTION_MOUSE_HWHEEL.value - INTERCEPTION_FILTER_MOUSE_MOVE = 0x1000 - -class interception_mouse_flag(Enum): - INTERCEPTION_MOUSE_MOVE_RELATIVE = 0x000 - INTERCEPTION_MOUSE_MOVE_ABSOLUTE = 0x001 - INTERCEPTION_MOUSE_VIRTUAL_DESKTOP = 0x002 - INTERCEPTION_MOUSE_ATTRIBUTES_CHANGED = 0x004 - INTERCEPTION_MOUSE_MOVE_NOCOALESCE = 0x008 - INTERCEPTION_MOUSE_TERMSRV_SRC_SHADOW = 0x100 \ No newline at end of file diff --git a/interception.py b/interception.py deleted file mode 100644 index ce16cec..0000000 --- a/interception.py +++ /dev/null @@ -1,182 +0,0 @@ -from ctypes import * -from stroke import * -from consts import * - -MAX_DEVICES = 20 -MAX_KEYBOARD = 10 -MAX_MOUSE = 10 - -k32 = windll.LoadLibrary('kernel32') - -class interception: - _context: list = [] - k32 = None - _c_events = (c_void_p * MAX_DEVICES)() - - def __init__(self): - try: - for i in range(MAX_DEVICES): - _device = device(k32.CreateFileA(b'\\\\.\\interception%02d' % i, - 0x80000000,0,0,3,0,0), - k32.CreateEventA(0, 1, 0, 0), - interception.is_keyboard(i)) - self._context.append(_device) - self._c_events[i] = _device.event - - except Exception as e: - self._destroy_context() - raise e - - def wait(self,milliseconds =-1): - - result = k32.WaitForMultipleObjects(MAX_DEVICES,self._c_events,0,milliseconds) - if result == -1 or result == 0x102: - return 0 - else: - return result - - def set_filter(self,predicate,filter): - for i in range(MAX_DEVICES): - if predicate(i): - result = self._context[i].set_filter(filter) - - def get_HWID(self,device:int): - if not interception.is_invalid(device): - try: - return self._context[device].get_HWID().decode("utf-16") - except: - pass - return "" - - def receive(self,device:int): - if not interception.is_invalid(device): - return self._context[device].receive() - - def send(self,device: int,stroke : stroke): - if not interception.is_invalid(device): - self._context[device].send(stroke) - - @staticmethod - def is_keyboard(device): - return device+1 > 0 and device+1 <= MAX_KEYBOARD - - @staticmethod - def is_mouse(device): - return device+1 > MAX_KEYBOARD and device+1 <= MAX_KEYBOARD + MAX_MOUSE - - @staticmethod - def is_invalid(device): - return device+1 <= 0 or device+1 > (MAX_KEYBOARD + MAX_MOUSE) - - def _destroy_context(self): - for device in self._context: - device.destroy() - -class device_io_result: - result = 0 - data = None - data_bytes = None - def __init__(self,result,data): - self.result = result - if data!=None: - self.data = list(data) - self.data_bytes = bytes(data) - - -def device_io_call(decorated): - def decorator(device,*args,**kwargs): - command,inbuffer,outbuffer = decorated(device,*args,**kwargs) - return device._device_io_control(command,inbuffer,outbuffer) - return decorator - -class device(): - handle=0 - event=0 - is_keyboard = False - _parser = None - _bytes_returned = (c_int * 1)(0) - _c_byte_500 = (c_byte * 500)() - _c_int_2 = (c_int * 2)() - _c_ushort_1 = (c_ushort * 1)() - _c_int_1 = (c_int * 1)() - _c_recv_buffer = None - - def __init__(self, handle, event,is_keyboard:bool): - self.is_keyboard = is_keyboard - if is_keyboard: - self._c_recv_buffer = (c_byte * 12)() - self._parser = key_stroke - else: - self._c_recv_buffer = (c_byte * 24)() - self._parser = mouse_stroke - - if handle == -1 or event == 0: - raise Exception("Can't create device") - self.handle=handle - self.event =event - - if self._device_set_event().result == 0: - raise Exception("Can't communicate with driver") - - def destroy(self): - if self.handle != -1: - k32.CloseHandle(self.handle) - if self.event!=0: - k32.CloseHandle(self.event) - - @device_io_call - def get_precedence(self): - return 0x222008,0,self._c_int_1 - - @device_io_call - def set_precedence(self,precedence : int): - self._c_int_1[0] = precedence - return 0x222004,self._c_int_1,0 - - @device_io_call - def get_filter(self): - return 0x222020,0,self._c_ushort_1 - - @device_io_call - def set_filter(self,filter): - self._c_ushort_1[0] = filter - return 0x222010,self._c_ushort_1,0 - - @device_io_call - def _get_HWID(self): - return 0x222200,0,self._c_byte_500 - - def get_HWID(self): - data = self._get_HWID().data_bytes - return data[:self._bytes_returned[0]] - - @device_io_call - def _receive(self): - return 0x222100,0,self._c_recv_buffer - - def receive(self): - data = self._receive().data_bytes - return self._parser.parse_raw(data) - - def send(self,stroke:stroke): - if type(stroke) == self._parser: - self._send(stroke) - - @device_io_call - def _send(self,stroke:stroke): - memmove(self._c_recv_buffer,stroke.data_raw,len(self._c_recv_buffer)) - return 0x222080,self._c_recv_buffer,0 - - @device_io_call - def _device_set_event(self): - self._c_int_2[0] = self.event - return 0x222040,self._c_int_2,0 - - def _device_io_control(self,command,inbuffer,outbuffer)->device_io_result: - res = k32.DeviceIoControl(self.handle,command,inbuffer, - len(bytes(inbuffer)) if inbuffer != 0 else 0, - outbuffer, - len(bytes(outbuffer)) if outbuffer !=0 else 0, - self._bytes_returned,0) - - return device_io_result(res,outbuffer if outbuffer !=0 else None) \ No newline at end of file diff --git a/src/interception/__init__.py b/src/interception/__init__.py index 18035be..abaf013 100644 --- a/src/interception/__init__.py +++ b/src/interception/__init__.py @@ -1,2 +1,4 @@ from .interception import Interception -from .device import Device \ No newline at end of file +from .device import Device +from .strokes import KeyStroke, MouseStroke, Stroke +from .consts import * \ No newline at end of file diff --git a/src/interception/consts.py b/src/interception/consts.py new file mode 100644 index 0000000..389e6be --- /dev/null +++ b/src/interception/consts.py @@ -0,0 +1,65 @@ +from enum import IntEnum + +class KeyState(IntEnum): + KEY_DOWN = 0x00 + KEY_UP = 0x01 + KEY_E0 = 0x02 + KEY_E1 = 0x04 + KEY_TERMSRV_SET_LED = 0x08 + KEY_TERMSRV_SHADOW = 0x10 + KEY_TERMSRV_VKPACKET = 0x20 + +class FilterKeyState(IntEnum): + FILTER_KEY_NONE = 0x0000 + FILTER_KEY_ALL = 0xFFFF + FILTER_KEY_DOWN = KeyState.KEY_UP + FILTER_KEY_UP = KeyState.KEY_UP << 1 + FILTER_KEY_E0 = KeyState.KEY_E0 << 1 + FILTER_KEY_E1 = KeyState.KEY_E1 << 1 + FILTER_KEY_TERMSRV_SET_LED = KeyState.KEY_TERMSRV_SET_LED << 1 + FILTER_KEY_TERMSRV_SHADOW = KeyState.KEY_TERMSRV_SHADOW << 1 + FILTER_KEY_TERMSRV_VKPACKET = KeyState.KEY_TERMSRV_VKPACKET << 1 + +class MouseState(IntEnum): + MOUSE_LEFT_BUTTON_DOWN = 0x001 + MOUSE_LEFT_BUTTON_UP = 0x002 + MOUSE_RIGHT_BUTTON_DOWN = 0x004 + MOUSE_RIGHT_BUTTON_UP = 0x008 + MOUSE_MIDDLE_BUTTON_DOWN = 0x010 + MOUSE_MIDDLE_BUTTON_UP = 0x020 + + MOUSE_BUTTON_4_DOWN = 0x040 + MOUSE_BUTTON_4_UP = 0x080 + MOUSE_BUTTON_5_DOWN = 0x100 + MOUSE_BUTTON_5_UP = 0x200 + + MOUSE_WHEEL = 0x400 + MOUSE_HWHEEL = 0x800 + +class FilterMouseState(IntEnum): + FILTER_MOUSE_NONE = 0x0000 + FILTER_MOUSE_ALL = 0xFFFF + + FILTER_MOUSE_LEFT_BUTTON_DOWN = MouseState.MOUSE_LEFT_BUTTON_DOWN + FILTER_MOUSE_LEFT_BUTTON_UP = MouseState.MOUSE_LEFT_BUTTON_UP + FILTER_MOUSE_RIGHT_BUTTON_DOWN = MouseState.MOUSE_RIGHT_BUTTON_DOWN + FILTER_MOUSE_RIGHT_BUTTON_UP = MouseState.MOUSE_RIGHT_BUTTON_UP + FILTER_MOUSE_MIDDLE_BUTTON_DOWN = MouseState.MOUSE_MIDDLE_BUTTON_DOWN + FILTER_MOUSE_MIDDLE_BUTTON_UP = MouseState.MOUSE_MIDDLE_BUTTON_UP + + FILTER_MOUSE_BUTTON_4_DOWN = MouseState.MOUSE_BUTTON_4_DOWN + FILTER_MOUSE_BUTTON_4_UP = MouseState.MOUSE_BUTTON_4_UP + FILTER_MOUSE_BUTTON_5_DOWN = MouseState.MOUSE_BUTTON_5_DOWN + FILTER_MOUSE_BUTTON_5_UP = MouseState.MOUSE_BUTTON_5_UP + + FILTER_MOUSE_WHEEL = MouseState.MOUSE_WHEEL + FILTER_MOUSE_HWHEEL = MouseState.MOUSE_HWHEEL + FILTER_MOUSE_MOVE = 0x1000 + +class MouseFlag(IntEnum): + MOUSE_MOVE_RELATIVE = 0x000 + MOUSE_MOVE_ABSOLUTE = 0x001 + MOUSE_VIRTUAL_DESKTOP = 0x002 + MOUSE_ATTRIBUTES_CHANGED = 0x004 + MOUSE_MOVE_NOCOALESCE = 0x008 + MOUSE_TERMSRV_SRC_SHADOW = 0x100 \ No newline at end of file diff --git a/src/interception/device.py b/src/interception/device.py index 4d4e7ac..0e31392 100644 --- a/src/interception/device.py +++ b/src/interception/device.py @@ -1,3 +1,4 @@ +from __future__ import annotations from ctypes import c_byte, c_int, c_ushort, memmove, windll from dataclasses import dataclass, field from typing import Any, Type @@ -96,7 +97,9 @@ def receive(self): def send(self, stroke: Stroke): if not isinstance(stroke, self._parser): - raise ValueError(f"Can't parse {stroke} with {type(self._parser)}!") + raise ValueError( + f"Can't parse {stroke} with {self._parser.__name__} parser!" + ) self._send(stroke) @device_io_call diff --git a/src/interception/interception.py b/src/interception/interception.py index b674cbd..c4234bc 100644 --- a/src/interception/interception.py +++ b/src/interception/interception.py @@ -1,7 +1,5 @@ from ctypes import * -from ctypes import _NamedFuncPointer from typing import Final - from consts import * @@ -59,6 +57,21 @@ def build_handles(self) -> None: self._c_events[device_num] = device.event def wait(self, milliseconds: int = -1): + """Waits for input on any interception device. + + Calls the `WaitForMultipleObjects()` Windows API function to wait for input on any of the + interception devices. The function will block until input is available on one of the devices + or until the specified timeout period (in milliseconds) has elapsed. If `milliseconds` is + not specified or is negative, the function will block indefinitely until input is available. + + If input is available on a device, the function will return the index of the device in the + `_c_events` dictionary, indicating which device received input. If the timeout period + elapses before input is available, the function will return 0. If an error occurs, the function + will raise an OSError. + + Raises: + OSError: If an error occurs while waiting for input. + """ result = k32.WaitForMultipleObjects( MAX_DEVICES, self._c_events, 0, milliseconds ) @@ -67,6 +80,7 @@ def wait(self, milliseconds: int = -1): return result def get_HWID(self, device: int): + """Returns the HWID of a device in the context""" if self.is_invalid(device): return "" try: @@ -95,7 +109,7 @@ def is_invalid(device): return device + 1 <= 0 or device + 1 > (MAX_KEYBOARD + MAX_MOUSE) @staticmethod - def create_device_handle(device_num: int) -> _NamedFuncPointer: + def create_device_handle(device_num: int): """Creates a handle to a specified device. Access mode for the device is `GENERIC_READ | GENERIC_WRITE`, allows the diff --git a/src/interception/strokes.py b/src/interception/strokes.py index dad3be6..b7a7369 100644 --- a/src/interception/strokes.py +++ b/src/interception/strokes.py @@ -1,8 +1,12 @@ import struct -from typing import Protocol +from typing import Protocol, ClassVar +from dataclasses import dataclass +@dataclass class Stroke(Protocol): + fmt: ClassVar + fmt_raw: ClassVar @classmethod def parse(cls, self): @@ -21,17 +25,17 @@ def raw_data(self): ... +@dataclass class MouseStroke: - fmt = "HHhiiI" - fmt_raw = "HHHHIiiI" + fmt: ClassVar = "HHhiiI" + fmt_raw: ClassVar = "HHHHIiiI" - def __init__(self, state, flags, rolling, x, y, information): - self.state = state - self.flags = flags - self.rolling = rolling - self.x = x - self.y = y - self.information = information + state: int + flags: int + rolling: int + x: int + y: int + information: int @classmethod def parse(cls, data): @@ -68,15 +72,15 @@ def raw_data(self) -> bytes: self.information, ) - +@dataclass class KeyStroke: - fmt = "HHI" - fmt_raw = "HHHHI" - def __init__(self, code, state, information): - self.code = code - self.state = state - self.information = information + fmt: ClassVar = "HHI" + fmt_raw: ClassVar = "HHHHI" + + code: int + stage: int + information: int @classmethod def parse(cls, data): diff --git a/stroke.py b/stroke.py deleted file mode 100644 index d1da17f..0000000 --- a/stroke.py +++ /dev/null @@ -1,105 +0,0 @@ -import struct - -class stroke(): - - @property - def data(self): - raise NotImplementedError - - @property - def data_raw(self): - raise NotImplementedError - - -class mouse_stroke(stroke): - - fmt = 'HHhiiI' - fmt_raw = 'HHHHIiiI' - state = 0 - flags = 0 - rolling = 0 - x = 0 - y = 0 - information = 0 - - def __init__(self,state,flags,rolling,x,y,information): - super().__init__() - self.state =state - self.flags = flags - self.rolling = rolling - self.x = x - self.y = y - self.information = information - - @staticmethod - def parse(data): - return mouse_stroke(*struct.unpack(mouse_stroke.fmt,data)) - - @staticmethod - def parse_raw(data): - unpacked= struct.unpack(mouse_stroke.fmt_raw,data) - return mouse_stroke( - unpacked[2], - unpacked[1], - unpacked[3], - unpacked[5], - unpacked[6], - unpacked[7]) - - @property - def data(self): - data = struct.pack(self.fmt, - self.state, - self.flags, - self.rolling, - self.x, - self.y, - self.information) - return data - - @property - def data_raw(self): - data = struct.pack(self.fmt_raw, - 0, - self.flags, - self.state, - self.rolling, - 0, - self.x, - self.y, - self.information) - - return data - -class key_stroke(stroke): - - fmt = 'HHI' - fmt_raw = 'HHHHI' - code = 0 - state = 0 - information = 0 - - def __init__(self,code,state,information): - super().__init__() - self.code = code - self.state = state - self.information = information - - - @staticmethod - def parse(data): - return key_stroke(*struct.unpack(key_stroke.fmt,data)) - - @staticmethod - def parse_raw(data): - unpacked= struct.unpack(key_stroke.fmt_raw,data) - return key_stroke(unpacked[1],unpacked[2],unpacked[4]) - - @property - def data(self): - data = struct.pack(self.fmt,self.code,self.state,self.information) - return data - @property - def data_raw(self): - data = struct.pack(self.fmt_raw,0,self.code,self.state,0,self.information) - return data \ No newline at end of file From 75faa06381f9ae022e43c41769dd557862b0c9a5 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Tue, 11 Apr 2023 22:11:49 +0200 Subject: [PATCH 03/37] turned strokes into dataclasses --- .gitignore | 2 +- src/interception/device.py | 6 ++++++ src/interception/interception.py | 26 ++++++++++++-------------- src/interception/strokes.py | 4 ++-- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 747cd1d..91c4dfa 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ *.user *.userosscache *.sln.docstates -test.py +test # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/src/interception/device.py b/src/interception/device.py index 0e31392..96bfb2f 100644 --- a/src/interception/device.py +++ b/src/interception/device.py @@ -54,6 +54,12 @@ def __init__(self, handle, event, *, is_keyboard: bool): if self._device_set_event().result == 0: raise Exception("Can't communicate with driver") + def __str__(self) -> str: + return f"Device(handle={self.handle}, event={self.event}, is_keyboard: {self.is_keyboard})" + + def __repr__(self) -> str: + return f"Device(handle={self.handle}, event={self.event}, is_keyboard: {self.is_keyboard})" + def destroy(self): if self.handle != -1: k32.CloseHandle(self.handle) diff --git a/src/interception/interception.py b/src/interception/interception.py index c4234bc..e6c3874 100644 --- a/src/interception/interception.py +++ b/src/interception/interception.py @@ -1,7 +1,5 @@ -from ctypes import * +from ctypes import c_void_p, windll, Array from typing import Final -from consts import * - MAX_DEVICES: Final = 20 MAX_KEYBOARD: Final = 10 @@ -14,19 +12,19 @@ class Interception: - _context: list[Device] = [] - _c_events: Array[c_void_p] = (c_void_p * MAX_DEVICES)() + def __init__(self) -> None: + self._context: list[Device] = [] + self._c_events: Array[c_void_p] = (c_void_p * MAX_DEVICES)() - def __init__(self): try: self.build_handles() except IOError as e: self._destroy_context() raise e - + def build_handles(self) -> None: """Creates handles and events for all interception devices. - + Iterates over all interception devices and creates a `Device` object for each one. A `Device` object represents an interception device and includes a handle to the device, an event that can be used to wait for input on the device, and a flag indicating whether @@ -59,13 +57,13 @@ def build_handles(self) -> None: def wait(self, milliseconds: int = -1): """Waits for input on any interception device. - Calls the `WaitForMultipleObjects()` Windows API function to wait for input on any of the + Calls the `WaitForMultipleObjects()` Windows API function to wait for input on any of the interception devices. The function will block until input is available on one of the devices - or until the specified timeout period (in milliseconds) has elapsed. If `milliseconds` is + or until the specified timeout period (in milliseconds) has elapsed. If `milliseconds` is not specified or is negative, the function will block indefinitely until input is available. - If input is available on a device, the function will return the index of the device in the - `_c_events` dictionary, indicating which device received input. If the timeout period + If input is available on a device, the function will return the index of the device in the + `_c_events` dictionary, indicating which device received input. If the timeout period elapses before input is available, the function will return 0. If an error occurs, the function will raise an OSError. @@ -111,8 +109,8 @@ def is_invalid(device): @staticmethod def create_device_handle(device_num: int): """Creates a handle to a specified device. - - Access mode for the device is `GENERIC_READ | GENERIC_WRITE`, allows the + + Access mode for the device is `GENERIC_READ | GENERIC_WRITE`, allows the handle to read and write to the device. Sharing mode for the device is `FILE_SHARE_READ | FILE_SHARE_WRITE`, which diff --git a/src/interception/strokes.py b/src/interception/strokes.py index b7a7369..9f8f6b8 100644 --- a/src/interception/strokes.py +++ b/src/interception/strokes.py @@ -72,14 +72,14 @@ def raw_data(self) -> bytes: self.information, ) + @dataclass class KeyStroke: - fmt: ClassVar = "HHI" fmt_raw: ClassVar = "HHHHI" code: int - stage: int + state: int information: int @classmethod From 1e33e15ca7d7de775921272b3dfb1ad10769bd87 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Tue, 11 Apr 2023 22:22:55 +0200 Subject: [PATCH 04/37] Update README.md --- README.md | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 6dbeb3a..9682a8f 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,20 @@ -# interception_py -This is a port (not a [wrapper][wrp]) of [interception][c_ception] dll to python, it communicates directly with interception's driver +# pyintercept +This is a greatly reworked fork of [interception_py][wrp], a python port for [interception][c_ception]. -### why not using the wrapper? -* it's very slow and some strokes are lost -* fast strokes made python crash (some heap allocation errors) +The Interception API aims to build a portable programming interface that allows one to intercept and control a range of input devices. -To make it run you should install the driver from [c-interception][c_ception] -### example -```py +## Why use this fork instead of intercept_py? +- Interception_py has not been maintained in 4 years +- I made it as simple to use as things like pyautogui / pydirectinput +- I made it alot more clear and readable what is happening and where. +Unfortunately, the original repository is entirely undocumented and stuffed into one file for the most part. -from interception import * -if __name__ == "__main__": - c = interception() - c.set_filter(interception.is_keyboard,interception_filter_key_state.INTERCEPTION_FILTER_KEY_UP.value) - while True: - device = c.wait() - stroke = c.receive(device) - if type(stroke) is key_stroke: - print(stroke.code) - c.send(device,stroke) -``` +## Requirements +You absolutely need to install the [interception-driver][c_ception], otherwise none of this will work. +Its as simple as running a .bat file and restarting your computer. -[wrp]: https://github.com/cobrce/interception_wrapper +[wrp]: https://github.com/cobrce/interception_py [c_ception]: https://github.com/oblitum/Interception From d2c5889642dfa91086c13d4c987bddb245fc0bbf Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Wed, 12 Apr 2023 00:42:57 +0200 Subject: [PATCH 05/37] added first api functions --- src/interception/__init__.py | 2 +- src/interception/{consts.py => _consts.py} | 0 src/interception/_keycodes.py | 4 +- src/interception/inputs.py | 140 +++++++++++++++++++++ src/interception/interception.py | 7 +- test.py | 1 - 6 files changed, 149 insertions(+), 5 deletions(-) rename src/interception/{consts.py => _consts.py} (100%) create mode 100644 src/interception/inputs.py delete mode 100644 test.py diff --git a/src/interception/__init__.py b/src/interception/__init__.py index abaf013..1358888 100644 --- a/src/interception/__init__.py +++ b/src/interception/__init__.py @@ -1,4 +1,4 @@ from .interception import Interception from .device import Device from .strokes import KeyStroke, MouseStroke, Stroke -from .consts import * \ No newline at end of file +from .inputs import * \ No newline at end of file diff --git a/src/interception/consts.py b/src/interception/_consts.py similarity index 100% rename from src/interception/consts.py rename to src/interception/_consts.py diff --git a/src/interception/_keycodes.py b/src/interception/_keycodes.py index f6ea7a7..aafe40e 100644 --- a/src/interception/_keycodes.py +++ b/src/interception/_keycodes.py @@ -51,7 +51,7 @@ 'e': 0x12, 'r': 0x13, 't': 0x14, - 'y': 0x15, + 'y': 0x2C, 'u': 0x16, 'i': 0x17, 'o': 0x18, @@ -78,7 +78,7 @@ 'return': 0x1C, 'shift': 0x2A, 'shiftleft': 0x2A, - 'z': 0x2C, + 'z': 0x15, 'x': 0x2D, 'c': 0x2E, 'v': 0x2F, diff --git a/src/interception/inputs.py b/src/interception/inputs.py new file mode 100644 index 0000000..bba3356 --- /dev/null +++ b/src/interception/inputs.py @@ -0,0 +1,140 @@ +import time +from contextlib import contextmanager +from typing import Literal, Optional + +from win32api import GetSystemMetrics # type:ignore[import] + +from ._consts import * +from ._keycodes import KEYBOARD_MAPPING +from .interception import Interception +from .strokes import KeyStroke, MouseStroke + +interception = Interception() + +_screen_width = GetSystemMetrics(0) +_screen_height = GetSystemMetrics(1) + +MOUSE_BUTTON_MAPPING = { + "left": (MouseState.MOUSE_LEFT_BUTTON_DOWN, MouseState.MOUSE_LEFT_BUTTON_UP), + "right": (MouseState.MOUSE_RIGHT_BUTTON_DOWN, MouseState.MOUSE_RIGHT_BUTTON_UP), + "middle": (MouseState.MOUSE_MIDDLE_BUTTON_DOWN, MouseState.MOUSE_MIDDLE_BUTTON_UP), + "mouse4": (MouseState.MOUSE_BUTTON_4_DOWN, MouseState.MOUSE_BUTTON_4_UP), + "mouse5": (MouseState.MOUSE_BUTTON_5_DOWN, MouseState.MOUSE_BUTTON_5_UP), +} + + +def _normalize(x: int | tuple[int, int], y: Optional[int] = None) -> tuple[int, int]: + if isinstance(x, tuple): + if len(x) == 2: + x, y = x + elif len(x) == 4: + x, y, *_ = x + else: + raise ValueError(f"Cant normalize tuple of length {len(x)}: {x}") + else: + assert y is not None + + return int(x), int(y) + + +def _to_hexadecimal(screen_size: int, i: int): + return int((0xFFFF / screen_size) * i) + 1 + + +def _to_interception_point(x: int, y: int) -> tuple[int, int]: + return ( + _to_hexadecimal(_screen_width, x), + _to_hexadecimal(_screen_height, y), + ) + + +def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: + x, y = _normalize(x, y) + x, y = _to_interception_point(x, y) + + stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, x, y, 0) + interception.send(14, stroke) + + +def click( + x: Optional[int | tuple[int, int]] = None, + y: Optional[int] = None, + button: Literal["left", "right", "middle", "mouse4", "mouse5"] = "left", + clicks: int = 1, + interval: int | float = 0.1, + delay: int | float = 0.1, +) -> None: + if x is not None: + move_to(x, y) + + time.sleep(delay) + + for _ in range(clicks): + mouse_down(button) + mouse_up(button) + + if clicks > 1: + time.sleep(interval) + + +def press(key: str, presses: int = 1, interval: int | float = 0.1) -> None: + for _ in range(presses): + try: + key_down(key) + key_up(key) + except KeyError: + raise ValueError(f"Unsupported key, {key}") + if presses > 1: + time.sleep(interval) + + +def write(term: str, interval: int | float = 0.05) -> None: + for c in term.lower(): + press(c) + time.sleep(interval) + + +def key_down(key: str) -> None: + stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_DOWN, 0) + interception.send(1, stroke) + time.sleep(0.025) + + +def key_up(key: str) -> None: + stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_UP, 0) + interception.send(1, stroke) + time.sleep(0.025) + + +def mouse_down(button: Literal["left", "right", "middle", "mouse4", "mouse5"]) -> None: + down, _ = MOUSE_BUTTON_MAPPING[button] + + stroke = MouseStroke(down, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) + interception.send(14, stroke) + time.sleep(0.025) + + +def mouse_up(button: Literal["left", "right", "middle", "mouse4", "mouse5"]) -> None: + _, up = MOUSE_BUTTON_MAPPING[button] + + stroke = MouseStroke(up, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) + interception.send(14, stroke) + time.sleep(0.025) + + +@contextmanager +def hold_mouse(button: Literal["left", "right", "middle", "mouse4", "mouse5"]): + mouse_down(button=button) + try: + yield + finally: + mouse_up(button=button) + + +@contextmanager +def hold_key(key: str): + key_down(key) + try: + yield + finally: + key_up(key) diff --git a/src/interception/interception.py b/src/interception/interception.py index e6c3874..8ba330a 100644 --- a/src/interception/interception.py +++ b/src/interception/interception.py @@ -1,4 +1,4 @@ -from ctypes import c_void_p, windll, Array +from ctypes import Array, c_void_p, windll from typing import Final MAX_DEVICES: Final = 20 @@ -93,6 +93,11 @@ def receive(self, device: int): def send(self, device: int, stroke: Stroke): if not self.is_invalid(device): self._context[device].send(stroke) + + def set_filter(self,predicate,filter): + for i in range(MAX_DEVICES): + if predicate(i): + result = self._context[i].set_filter(filter) @staticmethod def is_keyboard(device): diff --git a/test.py b/test.py deleted file mode 100644 index 70dba21..0000000 --- a/test.py +++ /dev/null @@ -1 +0,0 @@ -from src.interception import inter \ No newline at end of file From c9bb4bc1d9a4718838be2fa7f0170c551414e32b Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Wed, 12 Apr 2023 00:51:05 +0200 Subject: [PATCH 06/37] added packaging --- pyproject.toml | 22 ++++++++++++ setup.cfg | 17 ++++++++++ setup.py | 3 ++ src/interception/device.py | 1 + src/pyintercept.egg-info/PKG-INFO | 34 +++++++++++++++++++ src/pyintercept.egg-info/SOURCES.txt | 17 ++++++++++ src/pyintercept.egg-info/dependency_links.txt | 1 + src/pyintercept.egg-info/not-zip-safe | 1 + src/pyintercept.egg-info/top_level.txt | 1 + 9 files changed, 97 insertions(+) create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 src/pyintercept.egg-info/PKG-INFO create mode 100644 src/pyintercept.egg-info/SOURCES.txt create mode 100644 src/pyintercept.egg-info/dependency_links.txt create mode 100644 src/pyintercept.egg-info/not-zip-safe create mode 100644 src/pyintercept.egg-info/top_level.txt diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4af37a0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyintercept" +version = "0.0.0" +authors = [ + { name="Kenny Hommel", email="kennyhommel36@gmail.com" }, +] +description = "Pyintercept" +readme = "README.md" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.urls] +"Homepage" = "https://github.com/kennyhml/pyintercept" +"Bug Tracker" = "https://github.com/kennyhml/pyintercept/issues" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ed0cdef --- /dev/null +++ b/setup.cfg @@ -0,0 +1,17 @@ +[metadata] +name = "pyintercept" +license_files = LICENSE + +[options] +package_dir= + =src +packages = find: +zip_safe = False +python_requires = >= 3 + + +[options.packages.find] +where = src +exclude = + tests* + .gitignore \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f4aeaa1 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup # type: ignore[import] + +setup() \ No newline at end of file diff --git a/src/interception/device.py b/src/interception/device.py index 96bfb2f..8396f4e 100644 --- a/src/interception/device.py +++ b/src/interception/device.py @@ -1,4 +1,5 @@ from __future__ import annotations + from ctypes import c_byte, c_int, c_ushort, memmove, windll from dataclasses import dataclass, field from typing import Any, Type diff --git a/src/pyintercept.egg-info/PKG-INFO b/src/pyintercept.egg-info/PKG-INFO new file mode 100644 index 0000000..2aa83e8 --- /dev/null +++ b/src/pyintercept.egg-info/PKG-INFO @@ -0,0 +1,34 @@ +Metadata-Version: 2.1 +Name: pyintercept +Version: 0.0.0 +Summary: Pyintercept +Author-email: Kenny Hommel +Project-URL: Homepage, https://github.com/kennyhml/pyintercept +Project-URL: Bug Tracker, https://github.com/kennyhml/pyintercept/issues +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE + +# pyintercept +This is a greatly reworked fork of [interception_py][wrp], a python port for [interception][c_ception]. + +The Interception API aims to build a portable programming interface that allows one to intercept and control a range of input devices. + + +## Why use this fork instead of intercept_py? +- Interception_py has not been maintained in 4 years +- I made it as simple to use as things like pyautogui / pydirectinput +- I made it alot more clear and readable what is happening and where. +Unfortunately, the original repository is entirely undocumented and stuffed into one file for the most part. + + +## Requirements +You absolutely need to install the [interception-driver][c_ception], otherwise none of this will work. + +Its as simple as running a .bat file and restarting your computer. + +[wrp]: https://github.com/cobrce/interception_py +[c_ception]: https://github.com/oblitum/Interception diff --git a/src/pyintercept.egg-info/SOURCES.txt b/src/pyintercept.egg-info/SOURCES.txt new file mode 100644 index 0000000..3f7e88d --- /dev/null +++ b/src/pyintercept.egg-info/SOURCES.txt @@ -0,0 +1,17 @@ +LICENSE +README.md +pyproject.toml +setup.cfg +setup.py +src/interception/__init__.py +src/interception/_consts.py +src/interception/_keycodes.py +src/interception/device.py +src/interception/inputs.py +src/interception/interception.py +src/interception/strokes.py +src/pyintercept.egg-info/PKG-INFO +src/pyintercept.egg-info/SOURCES.txt +src/pyintercept.egg-info/dependency_links.txt +src/pyintercept.egg-info/not-zip-safe +src/pyintercept.egg-info/top_level.txt \ No newline at end of file diff --git a/src/pyintercept.egg-info/dependency_links.txt b/src/pyintercept.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/pyintercept.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/pyintercept.egg-info/not-zip-safe b/src/pyintercept.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/pyintercept.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/src/pyintercept.egg-info/top_level.txt b/src/pyintercept.egg-info/top_level.txt new file mode 100644 index 0000000..fc7ae1b --- /dev/null +++ b/src/pyintercept.egg-info/top_level.txt @@ -0,0 +1 @@ +interception From d753d5becbf0c09e5fcd1ef05da0f02d121dd13f Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Wed, 12 Apr 2023 03:57:35 +0200 Subject: [PATCH 07/37] added new clicks --- src/interception/inputs.py | 16 +++++++++++++++- src/interception/strokes.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/interception/inputs.py b/src/interception/inputs.py index bba3356..fc81854 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -2,7 +2,7 @@ from contextlib import contextmanager from typing import Literal, Optional -from win32api import GetSystemMetrics # type:ignore[import] +from win32api import GetCursorPos, GetSystemMetrics # type:ignore[import] from ._consts import * from ._keycodes import KEYBOARD_MAPPING @@ -56,6 +56,12 @@ def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: interception.send(14, stroke) +def move_relative(x: int = 0, y: int = 0) -> None: + curr = GetCursorPos() + + move_to(curr[0] + x, curr[1] + y) + + def click( x: Optional[int | tuple[int, int]] = None, y: Optional[int] = None, @@ -77,6 +83,14 @@ def click( time.sleep(interval) +def left_click(clicks: int = 1, interval: int | float = 0.1) -> None: + click(button="left", clicks=clicks, interval=interval) + + +def right_click(clicks: int = 1, interval: int | float = 0.1) -> None: + click(button="right", clicks=clicks, interval=interval) + + def press(key: str, presses: int = 1, interval: int | float = 0.1) -> None: for _ in range(presses): try: diff --git a/src/interception/strokes.py b/src/interception/strokes.py index 9f8f6b8..acd0a19 100644 --- a/src/interception/strokes.py +++ b/src/interception/strokes.py @@ -44,7 +44,7 @@ def parse(cls, data): @classmethod def parse_raw(cls, data): unpacked = struct.unpack(cls.fmt_raw, data) - return cls(**(unpacked[i] for i in (2, 1, 3, 5, 6, 7))) + return cls(*(unpacked[i] for i in (2, 1, 3, 5, 6, 7))) @property def data(self) -> bytes: From bdde0b3e37ae35f156b59e6b8c268b7fc591ab08 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Wed, 12 Apr 2023 04:01:18 +0200 Subject: [PATCH 08/37] updated gitignore --- src/pyintercept.egg-info/PKG-INFO | 34 ------------------- src/pyintercept.egg-info/SOURCES.txt | 17 ---------- src/pyintercept.egg-info/dependency_links.txt | 1 - src/pyintercept.egg-info/not-zip-safe | 1 - src/pyintercept.egg-info/top_level.txt | 1 - 5 files changed, 54 deletions(-) delete mode 100644 src/pyintercept.egg-info/PKG-INFO delete mode 100644 src/pyintercept.egg-info/SOURCES.txt delete mode 100644 src/pyintercept.egg-info/dependency_links.txt delete mode 100644 src/pyintercept.egg-info/not-zip-safe delete mode 100644 src/pyintercept.egg-info/top_level.txt diff --git a/src/pyintercept.egg-info/PKG-INFO b/src/pyintercept.egg-info/PKG-INFO deleted file mode 100644 index 2aa83e8..0000000 --- a/src/pyintercept.egg-info/PKG-INFO +++ /dev/null @@ -1,34 +0,0 @@ -Metadata-Version: 2.1 -Name: pyintercept -Version: 0.0.0 -Summary: Pyintercept -Author-email: Kenny Hommel -Project-URL: Homepage, https://github.com/kennyhml/pyintercept -Project-URL: Bug Tracker, https://github.com/kennyhml/pyintercept/issues -Classifier: Programming Language :: Python :: 3 -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: OS Independent -Requires-Python: >=3.10 -Description-Content-Type: text/markdown -License-File: LICENSE - -# pyintercept -This is a greatly reworked fork of [interception_py][wrp], a python port for [interception][c_ception]. - -The Interception API aims to build a portable programming interface that allows one to intercept and control a range of input devices. - - -## Why use this fork instead of intercept_py? -- Interception_py has not been maintained in 4 years -- I made it as simple to use as things like pyautogui / pydirectinput -- I made it alot more clear and readable what is happening and where. -Unfortunately, the original repository is entirely undocumented and stuffed into one file for the most part. - - -## Requirements -You absolutely need to install the [interception-driver][c_ception], otherwise none of this will work. - -Its as simple as running a .bat file and restarting your computer. - -[wrp]: https://github.com/cobrce/interception_py -[c_ception]: https://github.com/oblitum/Interception diff --git a/src/pyintercept.egg-info/SOURCES.txt b/src/pyintercept.egg-info/SOURCES.txt deleted file mode 100644 index 3f7e88d..0000000 --- a/src/pyintercept.egg-info/SOURCES.txt +++ /dev/null @@ -1,17 +0,0 @@ -LICENSE -README.md -pyproject.toml -setup.cfg -setup.py -src/interception/__init__.py -src/interception/_consts.py -src/interception/_keycodes.py -src/interception/device.py -src/interception/inputs.py -src/interception/interception.py -src/interception/strokes.py -src/pyintercept.egg-info/PKG-INFO -src/pyintercept.egg-info/SOURCES.txt -src/pyintercept.egg-info/dependency_links.txt -src/pyintercept.egg-info/not-zip-safe -src/pyintercept.egg-info/top_level.txt \ No newline at end of file diff --git a/src/pyintercept.egg-info/dependency_links.txt b/src/pyintercept.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/src/pyintercept.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/pyintercept.egg-info/not-zip-safe b/src/pyintercept.egg-info/not-zip-safe deleted file mode 100644 index 8b13789..0000000 --- a/src/pyintercept.egg-info/not-zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/pyintercept.egg-info/top_level.txt b/src/pyintercept.egg-info/top_level.txt deleted file mode 100644 index fc7ae1b..0000000 --- a/src/pyintercept.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -interception From 75d3b4481877fba710bdb8b702f3b967991dfaa9 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Wed, 12 Apr 2023 04:02:13 +0200 Subject: [PATCH 09/37] updated gitignore --- .gitignore | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/.gitignore b/.gitignore index 91c4dfa..c0a96dc 100644 --- a/.gitignore +++ b/.gitignore @@ -329,3 +329,131 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ From dd64a17a2bf17f4cca3efa91c8b5890947f21172 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Wed, 12 Apr 2023 10:29:47 +0200 Subject: [PATCH 10/37] added listener functions --- src/interception/inputs.py | 47 ++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/src/interception/inputs.py b/src/interception/inputs.py index fc81854..4727e4c 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -14,6 +14,9 @@ _screen_width = GetSystemMetrics(0) _screen_height = GetSystemMetrics(1) +keyboard = 1 +mouse = 11 + MOUSE_BUTTON_MAPPING = { "left": (MouseState.MOUSE_LEFT_BUTTON_DOWN, MouseState.MOUSE_LEFT_BUTTON_UP), "right": (MouseState.MOUSE_RIGHT_BUTTON_DOWN, MouseState.MOUSE_RIGHT_BUTTON_UP), @@ -22,7 +25,6 @@ "mouse5": (MouseState.MOUSE_BUTTON_5_DOWN, MouseState.MOUSE_BUTTON_5_UP), } - def _normalize(x: int | tuple[int, int], y: Optional[int] = None) -> tuple[int, int]: if isinstance(x, tuple): if len(x) == 2: @@ -48,12 +50,38 @@ def _to_interception_point(x: int, y: int) -> tuple[int, int]: ) +def listen_to_keyboard() -> int: + interception.set_filter(interception.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) + + print("Waiting for a keyboard keypres...") + device = interception.wait() + stroke = interception.receive(device) + + print(f"Received stroke {stroke} on keyboard device {device}") + interception.send(device, stroke) + return device + + +def listen_to_mouse() -> int: + interception.set_filter( + interception.is_mouse, FilterMouseState.FILTER_MOUSE_LEFT_BUTTON_DOWN + ) + + print("Waiting for a mouse left click...") + device = interception.wait() + stroke = interception.receive(device) + + print(f"Received stroke {stroke} on mouse device {device}") + interception.send(device, stroke) + return device + + def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: x, y = _normalize(x, y) x, y = _to_interception_point(x, y) stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, x, y, 0) - interception.send(14, stroke) + interception.send(mouse, stroke) def move_relative(x: int = 0, y: int = 0) -> None: @@ -68,7 +96,7 @@ def click( button: Literal["left", "right", "middle", "mouse4", "mouse5"] = "left", clicks: int = 1, interval: int | float = 0.1, - delay: int | float = 0.1, + delay: int | float = 0.3, ) -> None: if x is not None: move_to(x, y) @@ -110,13 +138,14 @@ def write(term: str, interval: int | float = 0.05) -> None: def key_down(key: str) -> None: stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_DOWN, 0) - interception.send(1, stroke) + print(keyboard) + interception.send(keyboard, stroke) time.sleep(0.025) def key_up(key: str) -> None: stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_UP, 0) - interception.send(1, stroke) + interception.send(keyboard, stroke) time.sleep(0.025) @@ -124,16 +153,16 @@ def mouse_down(button: Literal["left", "right", "middle", "mouse4", "mouse5"]) - down, _ = MOUSE_BUTTON_MAPPING[button] stroke = MouseStroke(down, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) - interception.send(14, stroke) - time.sleep(0.025) + interception.send(mouse, stroke) + time.sleep(0.03) def mouse_up(button: Literal["left", "right", "middle", "mouse4", "mouse5"]) -> None: _, up = MOUSE_BUTTON_MAPPING[button] stroke = MouseStroke(up, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) - interception.send(14, stroke) - time.sleep(0.025) + interception.send(mouse, stroke) + time.sleep(0.03) @contextmanager From f2a1da056ba1065a461e8faaa506ba06356c7486 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Wed, 12 Apr 2023 11:14:09 +0200 Subject: [PATCH 11/37] Update README.md --- README.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9682a8f..b90f7e3 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,78 @@ This is a greatly reworked fork of [interception_py][wrp], a python port for [in The Interception API aims to build a portable programming interface that allows one to intercept and control a range of input devices. +## Why use interception? +Did you ever try to send inputs to an application or game and, well, nothing happened? Sure, in alot of cases this is resolved by running your +code with administrative privileges, but this is not always the case. -## Why use this fork instead of intercept_py? -- Interception_py has not been maintained in 4 years -- I made it as simple to use as things like pyautogui / pydirectinput -- I made it alot more clear and readable what is happening and where. -Unfortunately, the original repository is entirely undocumented and stuffed into one file for the most part. +Take parsec as example, you are entirely unable to send any simulated inputs to parsec, but it has no problems with real inputs, so why is this? +Long story short, injected inputs actually have a `LowLevelKeyHookInjected` (0x10) flag. This flag will **always** be set when sending an Input with `SendInput` or the even older `mouse_event` and `keyb_event`. This flag is not at all hard to pick up for programs, if you have python3.7 and pyHook installed, you can try it yourself using this code: -## Requirements -You absolutely need to install the [interception-driver][c_ception], otherwise none of this will work. +```py +import pyHook +import pythoncom -Its as simple as running a .bat file and restarting your computer. +def keyb_event(event): + + if event.flags & 0x10: + print("Injected input sent") + else: + print("Real input sent") + + return True + +hm = pyHook.HookManager() +hm.KeyDown = keyb_event + +hm.HookKeyboard() + +pythoncom.PumpMessages() +``` +You will quickly see that, no matter what conventional python library you try, all of them will be flagged as injected. Thats because in the end, they all either rely on `SendInput` or `keyb_event` | `mouse_event`. + +Why is this bad? Well, it's not always bad. If whatever you're sending inputs to currently works fine, and you are not worried about getting flagged by some sort of anti-cheat, then by all means its totally fine to stick to pyautogui / pydirectinput. + +Alright, enough about that, onto the important shit. + +## Why use this fork? +- I aim to make this port as simple to use as possible +- Comparable to things like pyautogui or pydirectinput +- The codebase has been refactored in a much more readable fashion +- I work with loads of automation first hand so there is alot of QoL features. + +Unfortunately, the original repository is entirely undocumented and stuffed into one file for the most part, +which made some things pretty hard to decipher and understand. + + +## How to use? +First of all, you absolutely need to install the [interception-driver][c_ception], otherwise none of this will work. Dont worry, is's as simple as running a .bat file and restarting your computer. + +Now, once you have all of that set up, you can go ahead and import `interception`. Let's start by identifying your used devices! + +```py +import interception + +kdevice = interception.listen_to_keyboard() +mdevice = interception.listen_to_mouse() +``` +You will get two integers back, those integers are the number of the device you just used. Let's set this device in interception to ensure it sends events from the correct one! +```py +interception.inputs.keyboard = kdevice +interception.inputs.mouse = mdevice +``` + +So, now you can begin to send inputs, just like you are used to it from pyautogui or pydirectinput! +```py +interception.move_to(960, 540) + +with interception.key_down("shift"): + interception.press("a") + +interception.click(120, 160, button="right", delay=1) +``` + +Have fun :D [wrp]: https://github.com/cobrce/interception_py [c_ception]: https://github.com/oblitum/Interception From c73ff0efa1acac015a5c3c93a7dc2cdf89f479e1 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Wed, 12 Apr 2023 11:18:36 +0200 Subject: [PATCH 12/37] added functions to capture devices --- src/interception/interception.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interception/interception.py b/src/interception/interception.py index 8ba330a..e2be722 100644 --- a/src/interception/interception.py +++ b/src/interception/interception.py @@ -1,13 +1,13 @@ from ctypes import Array, c_void_p, windll from typing import Final +from .device import Device +from .strokes import Stroke + MAX_DEVICES: Final = 20 MAX_KEYBOARD: Final = 10 MAX_MOUSE: Final = 10 -from .device import Device -from .strokes import Stroke - k32 = windll.LoadLibrary("kernel32") From 77095e7f8849f9e5e5e04470f06155816c53af14 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Fri, 14 Apr 2023 18:00:32 +0200 Subject: [PATCH 13/37] improved input interception --- src/interception/inputs.py | 49 +++++++++++++++++++++++++------------- src/interception/py.typed | 0 2 files changed, 32 insertions(+), 17 deletions(-) create mode 100644 src/interception/py.typed diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 4727e4c..cc10004 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -25,6 +25,7 @@ "mouse5": (MouseState.MOUSE_BUTTON_5_DOWN, MouseState.MOUSE_BUTTON_5_UP), } + def _normalize(x: int | tuple[int, int], y: Optional[int] = None) -> tuple[int, int]: if isinstance(x, tuple): if len(x) == 2: @@ -51,29 +52,44 @@ def _to_interception_point(x: int, y: int) -> tuple[int, int]: def listen_to_keyboard() -> int: - interception.set_filter(interception.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) + context = Interception() + context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) + + print("Waiting for a keyboard keypress...") - print("Waiting for a keyboard keypres...") - device = interception.wait() - stroke = interception.receive(device) + while True: + device = context.wait() + stroke = context.receive(device) - print(f"Received stroke {stroke} on keyboard device {device}") - interception.send(device, stroke) - return device + if stroke.code == 0x01: + print("ESC pressed, exited.") + context._destroy_context() + return device + + print(f"Received stroke {stroke} on keyboard device {device}") + context.send(device, stroke) def listen_to_mouse() -> int: - interception.set_filter( - interception.is_mouse, FilterMouseState.FILTER_MOUSE_LEFT_BUTTON_DOWN - ) + context = Interception() + context.set_filter(context.is_mouse, FilterMouseState.FILTER_MOUSE_LEFT_BUTTON_DOWN) + context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) + + print("Intercepting mouse left clicks, press ESC to quit.") + + while True: + device = context.wait() + stroke = context.receive(device) + + if context.is_keyboard(device) and stroke.code == 0x01: + print("ESC pressed, exited.") + context._destroy_context() + return device - print("Waiting for a mouse left click...") - device = interception.wait() - stroke = interception.receive(device) + elif not context.is_keyboard(device): + print(f"Received stroke {stroke} on mouse device {device}") - print(f"Received stroke {stroke} on mouse device {device}") - interception.send(device, stroke) - return device + context.send(device, stroke) def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: @@ -138,7 +154,6 @@ def write(term: str, interval: int | float = 0.05) -> None: def key_down(key: str) -> None: stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_DOWN, 0) - print(keyboard) interception.send(keyboard, stroke) time.sleep(0.025) diff --git a/src/interception/py.typed b/src/interception/py.typed new file mode 100644 index 0000000..e69de29 From 97484fe4b3ddff4b461b096b8a8a42cd74c408a2 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Fri, 14 Apr 2023 20:17:02 +0200 Subject: [PATCH 14/37] changed input function --- src/interception/inputs.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/interception/inputs.py b/src/interception/inputs.py index cc10004..15453d2 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -1,8 +1,10 @@ +import ctypes import time from contextlib import contextmanager +from ctypes import wintypes from typing import Literal, Optional -from win32api import GetCursorPos, GetSystemMetrics # type:ignore[import] +from win32api import GetSystemMetrics # type:ignore[import] from ._consts import * from ._keycodes import KEYBOARD_MAPPING @@ -51,12 +53,17 @@ def _to_interception_point(x: int, y: int) -> tuple[int, int]: ) +def _get_cursor_pos() -> tuple[int, int]: + cursor = wintypes.POINT() + ctypes.windll.user32.GetCursorPos(ctypes.byref(cursor)) + return (int(cursor.x), int(cursor.y)) + + def listen_to_keyboard() -> int: context = Interception() context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) print("Waiting for a keyboard keypress...") - while True: device = context.wait() stroke = context.receive(device) @@ -76,7 +83,6 @@ def listen_to_mouse() -> int: context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) print("Intercepting mouse left clicks, press ESC to quit.") - while True: device = context.wait() stroke = context.receive(device) @@ -101,11 +107,14 @@ def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: def move_relative(x: int = 0, y: int = 0) -> None: - curr = GetCursorPos() - + curr = _get_cursor_pos() move_to(curr[0] + x, curr[1] + y) +def position() -> tuple[int, int]: + return _get_cursor_pos() + + def click( x: Optional[int | tuple[int, int]] = None, y: Optional[int] = None, From b860cb51d8b48f9fdbdf2f057dee22e6559b5be8 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Fri, 14 Apr 2023 21:42:16 +0200 Subject: [PATCH 15/37] fixed catching bad error --- src/interception/inputs.py | 7 +++++-- src/interception/interception.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 15453d2..232ab8d 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -11,8 +11,11 @@ from .interception import Interception from .strokes import KeyStroke, MouseStroke -interception = Interception() - +try: + interception = Interception() +except IOError: + print("Failed to initialize Interception.") + _screen_width = GetSystemMetrics(0) _screen_height = GetSystemMetrics(1) diff --git a/src/interception/interception.py b/src/interception/interception.py index e2be722..0825b38 100644 --- a/src/interception/interception.py +++ b/src/interception/interception.py @@ -18,7 +18,7 @@ def __init__(self) -> None: try: self.build_handles() - except IOError as e: + except Exception as e: self._destroy_context() raise e From 3742965aa8e52fa87083135dec0f2189ce148e3f Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Fri, 14 Apr 2023 21:44:14 +0200 Subject: [PATCH 16/37] fixed bad error --- src/interception/inputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 232ab8d..ee3e464 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -13,7 +13,7 @@ try: interception = Interception() -except IOError: +except Exception: print("Failed to initialize Interception.") _screen_width = GetSystemMetrics(0) From dcae97b89b6b6d0d20dfcc8ec1dfaab7127da5b0 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Sat, 15 Apr 2023 07:56:37 +0200 Subject: [PATCH 17/37] use c_ubyte unsigned byte instead of c_byte --- src/interception/device.py | 9 ++++---- src/interception/inputs.py | 43 ++++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/interception/device.py b/src/interception/device.py index 8396f4e..1502417 100644 --- a/src/interception/device.py +++ b/src/interception/device.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ctypes import c_byte, c_int, c_ushort, memmove, windll +from ctypes import c_byte, c_int, c_ubyte, c_ushort, memmove, windll from dataclasses import dataclass, field from typing import Any, Type @@ -21,11 +21,12 @@ def decorator(device: Device, *args, **kwargs): class DeviceIOResult: result: int data: Any - data_bytes: bytes = field(init=False) + data_bytes: bytes = field(init=False, repr=False) def __post_init__(self): if self.data is not None: self.data = list(self.data) + print(self.data) self.data_bytes = bytes(self.data) @@ -40,10 +41,10 @@ def __init__(self, handle, event, *, is_keyboard: bool): self.is_keyboard = is_keyboard self._parser: Type[KeyStroke] | Type[MouseStroke] if is_keyboard: - self._c_recv_buffer = (c_byte * 12)() + self._c_recv_buffer = (c_ubyte * 12)() self._parser = KeyStroke else: - self._c_recv_buffer = (c_byte * 24)() + self._c_recv_buffer = (c_ubyte * 24)() self._parser = MouseStroke if handle == -1 or event == 0: diff --git a/src/interception/inputs.py b/src/interception/inputs.py index ee3e464..0390aed 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -67,18 +67,19 @@ def listen_to_keyboard() -> int: context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) print("Waiting for a keyboard keypress...") - while True: - device = context.wait() - stroke = context.receive(device) - - if stroke.code == 0x01: - print("ESC pressed, exited.") - context._destroy_context() - return device + try: + while True: + device = context.wait() + stroke = context.receive(device) - print(f"Received stroke {stroke} on keyboard device {device}") - context.send(device, stroke) + if stroke.code == 0x01: + print("ESC pressed, exited.") + return device + print(f"Received stroke {stroke} on keyboard device {device}") + context.send(device, stroke) + finally: + context._destroy_context() def listen_to_mouse() -> int: context = Interception() @@ -86,20 +87,22 @@ def listen_to_mouse() -> int: context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) print("Intercepting mouse left clicks, press ESC to quit.") - while True: - device = context.wait() - stroke = context.receive(device) + try: + while True: + device = context.wait() + stroke = context.receive(device) - if context.is_keyboard(device) and stroke.code == 0x01: - print("ESC pressed, exited.") - context._destroy_context() - return device + if context.is_keyboard(device) and stroke.code == 0x01: + print("ESC pressed, exited.") + return device - elif not context.is_keyboard(device): - print(f"Received stroke {stroke} on mouse device {device}") + elif not context.is_keyboard(device): + print(f"Received stroke {stroke} on mouse device {device}") - context.send(device, stroke) + context.send(device, stroke) + finally: + context._destroy_context() def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: x, y = _normalize(x, y) From b5f0c85ba24f4189fe76d05cb99dd48d36b22d26 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Mon, 17 Apr 2023 21:17:51 +0200 Subject: [PATCH 18/37] improved documentation of inputs --- src/interception/_consts.py | 12 ++- src/interception/device.py | 1 - src/interception/inputs.py | 161 ++++++++++++++++++++++++++++++++++-- 3 files changed, 167 insertions(+), 7 deletions(-) diff --git a/src/interception/_consts.py b/src/interception/_consts.py index 389e6be..c3c3ae6 100644 --- a/src/interception/_consts.py +++ b/src/interception/_consts.py @@ -1,5 +1,6 @@ from enum import IntEnum + class KeyState(IntEnum): KEY_DOWN = 0x00 KEY_UP = 0x01 @@ -9,6 +10,7 @@ class KeyState(IntEnum): KEY_TERMSRV_SHADOW = 0x10 KEY_TERMSRV_VKPACKET = 0x20 + class FilterKeyState(IntEnum): FILTER_KEY_NONE = 0x0000 FILTER_KEY_ALL = 0xFFFF @@ -20,6 +22,7 @@ class FilterKeyState(IntEnum): FILTER_KEY_TERMSRV_SHADOW = KeyState.KEY_TERMSRV_SHADOW << 1 FILTER_KEY_TERMSRV_VKPACKET = KeyState.KEY_TERMSRV_VKPACKET << 1 + class MouseState(IntEnum): MOUSE_LEFT_BUTTON_DOWN = 0x001 MOUSE_LEFT_BUTTON_UP = 0x002 @@ -36,6 +39,7 @@ class MouseState(IntEnum): MOUSE_WHEEL = 0x400 MOUSE_HWHEEL = 0x800 + class FilterMouseState(IntEnum): FILTER_MOUSE_NONE = 0x0000 FILTER_MOUSE_ALL = 0xFFFF @@ -56,10 +60,16 @@ class FilterMouseState(IntEnum): FILTER_MOUSE_HWHEEL = MouseState.MOUSE_HWHEEL FILTER_MOUSE_MOVE = 0x1000 + class MouseFlag(IntEnum): MOUSE_MOVE_RELATIVE = 0x000 MOUSE_MOVE_ABSOLUTE = 0x001 MOUSE_VIRTUAL_DESKTOP = 0x002 MOUSE_ATTRIBUTES_CHANGED = 0x004 MOUSE_MOVE_NOCOALESCE = 0x008 - MOUSE_TERMSRV_SRC_SHADOW = 0x100 \ No newline at end of file + MOUSE_TERMSRV_SRC_SHADOW = 0x100 + + +class MouseRolling(IntEnum): + MOUSE_WHEEL_UP = 0x78 + MOUSE_WHEEL_DOWN = 0xFF88 diff --git a/src/interception/device.py b/src/interception/device.py index 1502417..d0b6cba 100644 --- a/src/interception/device.py +++ b/src/interception/device.py @@ -26,7 +26,6 @@ class DeviceIOResult: def __post_init__(self): if self.data is not None: self.data = list(self.data) - print(self.data) self.data_bytes = bytes(self.data) diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 0390aed..371364c 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -15,7 +15,7 @@ interception = Interception() except Exception: print("Failed to initialize Interception.") - + _screen_width = GetSystemMetrics(0) _screen_height = GetSystemMetrics(1) @@ -32,6 +32,7 @@ def _normalize(x: int | tuple[int, int], y: Optional[int] = None) -> tuple[int, int]: + """Normalizes an x, y position to allow passing them seperately or as tuple.""" if isinstance(x, tuple): if len(x) == 2: x, y = x @@ -46,10 +47,14 @@ def _normalize(x: int | tuple[int, int], y: Optional[int] = None) -> tuple[int, def _to_hexadecimal(screen_size: int, i: int): + """Converts a coordinate from the usual system to a coordinate interception + can understand and move to.""" return int((0xFFFF / screen_size) * i) + 1 def _to_interception_point(x: int, y: int) -> tuple[int, int]: + """Converts a point (x, y) from the usual system to a point interception + can understand and move to.""" return ( _to_hexadecimal(_screen_width, x), _to_hexadecimal(_screen_height, y), @@ -57,16 +62,21 @@ def _to_interception_point(x: int, y: int) -> tuple[int, int]: def _get_cursor_pos() -> tuple[int, int]: + """Gets the current position of the cursor using `GetCursorPos`""" cursor = wintypes.POINT() ctypes.windll.user32.GetCursorPos(ctypes.byref(cursor)) return (int(cursor.x), int(cursor.y)) -def listen_to_keyboard() -> int: +def capture_keyboard() -> int: + """Captures keyboard keypresses until the `Escape` key is pressed. + + Filters out non `KEY_DOWN` events to not post the same capture twice. + """ context = Interception() context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) - print("Waiting for a keyboard keypress...") + print("Capturing keyboard presses, press ESC to quit.") try: while True: device = context.wait() @@ -81,12 +91,17 @@ def listen_to_keyboard() -> int: finally: context._destroy_context() -def listen_to_mouse() -> int: + +def capture_mouse() -> int: + """Captures mouse left clicks until the `Escape` key is pressed. + + Filters out non `LEFT_BUTTON_DOWN` events to not post the same capture twice. + """ context = Interception() context.set_filter(context.is_mouse, FilterMouseState.FILTER_MOUSE_LEFT_BUTTON_DOWN) context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) - print("Intercepting mouse left clicks, press ESC to quit.") + print("Capturing mouse left clicks, press ESC to quit.") try: while True: device = context.wait() @@ -100,11 +115,63 @@ def listen_to_mouse() -> int: print(f"Received stroke {stroke} on mouse device {device}") context.send(device, stroke) + finally: + context._destroy_context() + + +def listen_to_keyboard() -> int: + """Captures keyboard keypresses until the `Escape` key is pressed. + + Doesn't filter out any events at all. + """ + context = Interception() + context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_ALL) + + print("Listenting to keyboard, press ESC to quit.") + try: + while True: + device = context.wait() + stroke = context.receive(device) + + if stroke.code == 0x01: + print("ESC pressed, exited.") + return device + print(f"Received stroke {stroke} on keyboard device {device}") + context.send(device, stroke) finally: context._destroy_context() + +def listen_to_mouse() -> int: + """Captures mouse movements / clicks until the `Escape` key is pressed. + + Doesn't filter out any events at all. + """ + context = Interception() + context.set_filter(context.is_mouse, FilterMouseState.FILTER_MOUSE_ALL) + context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) + + print("Listenting to mouse, press ESC to quit.") + try: + while True: + device = context.wait() + stroke = context.receive(device) + + if context.is_keyboard(device) and stroke.code == 0x01: + print("ESC pressed, exited.") + return device + + elif not context.is_keyboard(device): + print(f"Received stroke {stroke} on mouse device {device}") + + context.send(device, stroke) + finally: + context._destroy_context() + + def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: + """Moves to a given position.""" x, y = _normalize(x, y) x, y = _to_interception_point(x, y) @@ -113,11 +180,13 @@ def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: def move_relative(x: int = 0, y: int = 0) -> None: + """Moves relatively to a given position.""" curr = _get_cursor_pos() move_to(curr[0] + x, curr[1] + y) def position() -> tuple[int, int]: + """Returns the position of the cursor on the monitor.""" return _get_cursor_pos() @@ -129,6 +198,22 @@ def click( interval: int | float = 0.1, delay: int | float = 0.3, ) -> None: + """Clicks at a given position. + + Parameters + ---------- + button :class:`Literal["left", "right", "middle", "mouse4", "mouse5"]`: + The button to click once moved to the location (if passed), default "left". + + clicks :class:`int`: + The amount of mouse clicks to perform with the given button + + interval :class:`int | float`: + The interval between multiple clicks, only applies if clicks > 1 + + delay :class:`int | float`: + The delay between moving and clicking. + """ if x is not None: move_to(x, y) @@ -143,14 +228,29 @@ def click( def left_click(clicks: int = 1, interval: int | float = 0.1) -> None: + """Left clicks at the current position.""" click(button="left", clicks=clicks, interval=interval) def right_click(clicks: int = 1, interval: int | float = 0.1) -> None: + """Right cicks at the current position.""" click(button="right", clicks=clicks, interval=interval) def press(key: str, presses: int = 1, interval: int | float = 0.1) -> None: + """Presses a key. + + Parameters + ---------- + key :class:`str`: + The key to press. + + presses :class:`int`: + The amount of presses to perform with the given key. + + interval :class:`int | float`: + The interval between multiple presses, only applies if presses > 1. + """ for _ in range(presses): try: key_down(key) @@ -162,24 +262,60 @@ def press(key: str, presses: int = 1, interval: int | float = 0.1) -> None: def write(term: str, interval: int | float = 0.05) -> None: + """Writes a term by sending each key one after another. + + Parameters + ---------- + term :class:`str`: + The term to write. + + interval :class:`int | float`: + The interval between pressing of the different characters. + """ for c in term.lower(): press(c) time.sleep(interval) +def scroll(direction: Literal["up", "down"]) -> None: + """Scrolls the mouse wheel one unit in a given direction.""" + amount = ( + MouseRolling.MOUSE_WHEEL_UP + if direction == "up" + else MouseRolling.MOUSE_WHEEL_DOWN + ) + + stroke = MouseStroke( + MouseState.MOUSE_WHEEL, MouseFlag.MOUSE_MOVE_RELATIVE, amount, 0, 0, 0 + ) + interception.send(mouse, stroke) + time.sleep(0.025) + + def key_down(key: str) -> None: + """Holds a key down, will not be released automatically. + + If you want to hold a key while performing an action, please use + `hold_key`, which offers a context manager. + """ stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_DOWN, 0) interception.send(keyboard, stroke) time.sleep(0.025) def key_up(key: str) -> None: + """Releases a key.""" stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_UP, 0) interception.send(keyboard, stroke) time.sleep(0.025) def mouse_down(button: Literal["left", "right", "middle", "mouse4", "mouse5"]) -> None: + """Holds a mouse button down, will not be released automatically. + + If you want to hold a mouse button while performing an action, please use + `hold_mouse`, which offers a context manager. + """ down, _ = MOUSE_BUTTON_MAPPING[button] stroke = MouseStroke(down, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) @@ -188,6 +324,7 @@ def mouse_down(button: Literal["left", "right", "middle", "mouse4", "mouse5"]) - def mouse_up(button: Literal["left", "right", "middle", "mouse4", "mouse5"]) -> None: + """Releases a mouse button.""" _, up = MOUSE_BUTTON_MAPPING[button] stroke = MouseStroke(up, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) @@ -197,6 +334,13 @@ def mouse_up(button: Literal["left", "right", "middle", "mouse4", "mouse5"]) -> @contextmanager def hold_mouse(button: Literal["left", "right", "middle", "mouse4", "mouse5"]): + """A context manager to hold a mouse button while performing another action. + + Example: + ```py + with interception.hold_mouse("left"): + interception.move_to(300, 300) + """ mouse_down(button=button) try: yield @@ -206,6 +350,13 @@ def hold_mouse(button: Literal["left", "right", "middle", "mouse4", "mouse5"]): @contextmanager def hold_key(key: str): + """A context manager to hold a mouse button while performing another action. + + Example: + ```py + with interception.hold_key("ctrl"): + interception.press("c") + """ key_down(key) try: yield From cde53cccbfe04d557c9b285f9fb078f3986da4fb Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Thu, 4 May 2023 11:53:33 +0200 Subject: [PATCH 19/37] added keyboard layout adjustment --- src/interception/_keycodes.py | 212 +++++++++++++++++----------------- src/interception/device.py | 3 +- 2 files changed, 110 insertions(+), 105 deletions(-) diff --git a/src/interception/_keycodes.py b/src/interception/_keycodes.py index aafe40e..9cf5a13 100644 --- a/src/interception/_keycodes.py +++ b/src/interception/_keycodes.py @@ -1,108 +1,112 @@ import ctypes +import win32api # type: ignore[import] KEYBOARD_MAPPING = { - 'escape': 0x01, - 'esc': 0x01, - 'f1': 0x3B, - 'f2': 0x3C, - 'f3': 0x3D, - 'f4': 0x3E, - 'f5': 0x3F, - 'f6': 0x40, - 'f7': 0x41, - 'f8': 0x42, - 'f9': 0x43, - 'f10': 0x44, - 'f11': 0x57, - 'f12': 0x58, - 'printscreen': 0xB7, - 'prntscrn': 0xB7, - 'prtsc': 0xB7, - 'prtscr': 0xB7, - 'scrolllock': 0x46, - 'pause': 0xC5, - '`': 0x29, - '1': 0x02, - '2': 0x03, - '3': 0x04, - '4': 0x05, - '5': 0x06, - '6': 0x07, - '7': 0x08, - '8': 0x09, - '9': 0x0A, - '0': 0x0B, - '-': 0x0C, - '=': 0x0D, - 'backspace': 0x0E, - 'insert': 0xD2 + 1024, - 'home': 0xC7 + 1024, - 'pageup': 0xC9 + 1024, - 'pagedown': 0xD1 + 1024, - 'numlock': 0x45, - 'divide': 0xB5 + 1024, - 'multiply': 0x37, - 'subtract': 0x4A, - 'add': 0x4E, - 'decimal': 0x53, - 'tab': 0x0F, - 'q': 0x10, - 'w': 0x11, - 'e': 0x12, - 'r': 0x13, - 't': 0x14, - 'y': 0x2C, - 'u': 0x16, - 'i': 0x17, - 'o': 0x18, - 'p': 0x19, - '[': 0x1A, - ']': 0x1B, - '\\': 0x2B, - 'del': 0xD3 + 1024, - 'delete': 0xD3 + 1024, - 'end': 0xCF + 1024, - 'capslock': 0x3A, - 'a': 0x1E, - 's': 0x1F, - 'd': 0x20, - 'f': 0x21, - 'g': 0x22, - 'h': 0x23, - 'j': 0x24, - 'k': 0x25, - 'l': 0x26, - ';': 0x27, + "escape": 0x01, + "esc": 0x01, + "f1": 0x3B, + "f2": 0x3C, + "f3": 0x3D, + "f4": 0x3E, + "f5": 0x3F, + "f6": 0x40, + "f7": 0x41, + "f8": 0x42, + "f9": 0x43, + "f10": 0x44, + "f11": 0x57, + "f12": 0x58, + "printscreen": 0xB7, + "prntscrn": 0xB7, + "prtsc": 0xB7, + "prtscr": 0xB7, + "scrolllock": 0x46, + "pause": 0xC5, + "`": 0x29, + "1": 0x02, + "2": 0x03, + "3": 0x04, + "4": 0x05, + "5": 0x06, + "6": 0x07, + "7": 0x08, + "8": 0x09, + "9": 0x0A, + "0": 0x0B, + "-": 0x0C, + "=": 0x0D, + "backspace": 0x0E, + "insert": 0xD2 + 1024, + "home": 0xC7 + 1024, + "pageup": 0xC9 + 1024, + "pagedown": 0xD1 + 1024, + "numlock": 0x45, + "divide": 0xB5 + 1024, + "multiply": 0x37, + "subtract": 0x4A, + "add": 0x4E, + "decimal": 0x53, + "tab": 0x0F, + "q": 0x10, + "w": 0x11, + "e": 0x12, + "r": 0x13, + "t": 0x14, + "y": 0x2C, + "u": 0x16, + "i": 0x17, + "o": 0x18, + "p": 0x19, + "[": 0x1A, + "]": 0x1B, + "\\": 0x2B, + "del": 0xD3 + 1024, + "delete": 0xD3 + 1024, + "end": 0xCF + 1024, + "capslock": 0x3A, + "a": 0x1E, + "s": 0x1F, + "d": 0x20, + "f": 0x21, + "g": 0x22, + "h": 0x23, + "j": 0x24, + "k": 0x25, + "l": 0x26, + ";": 0x27, "'": 0x28, - 'enter': 0x1C, - 'return': 0x1C, - 'shift': 0x2A, - 'shiftleft': 0x2A, - 'z': 0x15, - 'x': 0x2D, - 'c': 0x2E, - 'v': 0x2F, - 'b': 0x30, - 'n': 0x31, - 'm': 0x32, - ',': 0x33, - '.': 0x34, - '/': 0x35, - 'shiftright': 0x36, - 'ctrl': 0x1D, - 'ctrlleft': 0x1D, - 'win': 0xDB + 1024, - 'winleft': 0xDB + 1024, - 'alt': 0x38, - 'altleft': 0x38, - ' ': 0x39, - 'space': 0x39, - 'altright': 0xB8 + 1024, - 'winright': 0xDC + 1024, - 'apps': 0xDD + 1024, - 'ctrlright': 0x9D + 1024, - 'up': ctypes.windll.user32.MapVirtualKeyW(0x26, 0), - 'left': ctypes.windll.user32.MapVirtualKeyW(0x25, 0), - 'down': ctypes.windll.user32.MapVirtualKeyW(0x28, 0), - 'right': ctypes.windll.user32.MapVirtualKeyW(0x27, 0), -} \ No newline at end of file + "enter": 0x1C, + "return": 0x1C, + "shift": 0x2A, + "shiftleft": 0x2A, + "z": 0x15, + "x": 0x2D, + "c": 0x2E, + "v": 0x2F, + "b": 0x30, + "n": 0x31, + "m": 0x32, + ",": 0x33, + ".": 0x34, + "/": 0x35, + "shiftright": 0x36, + "ctrl": 0x1D, + "ctrlleft": 0x1D, + "win": 0xDB + 1024, + "winleft": 0xDB + 1024, + "alt": 0x38, + "altleft": 0x38, + " ": 0x39, + "space": 0x39, + "altright": 0xB8 + 1024, + "winright": 0xDC + 1024, + "apps": 0xDD + 1024, + "ctrlright": 0x9D + 1024, + "up": ctypes.windll.user32.MapVirtualKeyW(0x26, 0), + "left": ctypes.windll.user32.MapVirtualKeyW(0x25, 0), + "down": ctypes.windll.user32.MapVirtualKeyW(0x28, 0), + "right": ctypes.windll.user32.MapVirtualKeyW(0x27, 0), +} + +for c in range(32, 92): + KEYBOARD_MAPPING[chr(c).lower()] = win32api.MapVirtualKey(c, 0) diff --git a/src/interception/device.py b/src/interception/device.py index d0b6cba..b74f30e 100644 --- a/src/interception/device.py +++ b/src/interception/device.py @@ -19,6 +19,7 @@ def decorator(device: Device, *args, **kwargs): @dataclass class DeviceIOResult: + """Represents the result of an IO operation on a `Device`.""" result: int data: Any data_bytes: bytes = field(init=False, repr=False) @@ -92,7 +93,7 @@ def _get_HWID(self): def get_HWID(self): data = self._get_HWID().data_bytes - return data[: self._bytes_returned[0]] + return data[:self._bytes_returned[0]] @device_io_call def _receive(self): From 6a383fb161e8fa42371a9e57adfd9c1ebe8e1eba Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Thu, 18 May 2023 18:41:17 +0200 Subject: [PATCH 20/37] created utils and exceptions --- src/interception/_consts.py | 65 ++++--- src/interception/_utils.py | 56 ++++++ src/interception/exceptions.py | 21 +++ src/interception/inputs.py | 315 ++++++++++++++++----------------- src/interception/types.py | 3 + 5 files changed, 273 insertions(+), 187 deletions(-) create mode 100644 src/interception/_utils.py create mode 100644 src/interception/exceptions.py create mode 100644 src/interception/types.py diff --git a/src/interception/_consts.py b/src/interception/_consts.py index c3c3ae6..ec9dafd 100644 --- a/src/interception/_consts.py +++ b/src/interception/_consts.py @@ -1,4 +1,7 @@ +from __future__ import annotations from enum import IntEnum +from typing import Literal +from .exceptions import InvalidMouseButtonRequested class KeyState(IntEnum): @@ -11,18 +14,6 @@ class KeyState(IntEnum): KEY_TERMSRV_VKPACKET = 0x20 -class FilterKeyState(IntEnum): - FILTER_KEY_NONE = 0x0000 - FILTER_KEY_ALL = 0xFFFF - FILTER_KEY_DOWN = KeyState.KEY_UP - FILTER_KEY_UP = KeyState.KEY_UP << 1 - FILTER_KEY_E0 = KeyState.KEY_E0 << 1 - FILTER_KEY_E1 = KeyState.KEY_E1 << 1 - FILTER_KEY_TERMSRV_SET_LED = KeyState.KEY_TERMSRV_SET_LED << 1 - FILTER_KEY_TERMSRV_SHADOW = KeyState.KEY_TERMSRV_SHADOW << 1 - FILTER_KEY_TERMSRV_VKPACKET = KeyState.KEY_TERMSRV_VKPACKET << 1 - - class MouseState(IntEnum): MOUSE_LEFT_BUTTON_DOWN = 0x001 MOUSE_LEFT_BUTTON_UP = 0x002 @@ -39,6 +30,29 @@ class MouseState(IntEnum): MOUSE_WHEEL = 0x400 MOUSE_HWHEEL = 0x800 + @staticmethod + def from_string( + button: Literal["left", "right", "middle", "mouse4", "mouse5"] + ) -> tuple[MouseState, MouseState]: + try: + return _MAPPED_MOUSE_BUTTONS[button] + except KeyError: + raise InvalidMouseButtonRequested(button) + + +class MouseFlag(IntEnum): + MOUSE_MOVE_RELATIVE = 0x000F + MOUSE_MOVE_ABSOLUTE = 0x001 + MOUSE_VIRTUAL_DESKTOP = 0x002 + MOUSE_ATTRIBUTES_CHANGED = 0x004 + MOUSE_MOVE_NOCOALESCE = 0x008 + MOUSE_TERMSRV_SRC_SHADOW = 0x100 + + +class MouseRolling(IntEnum): + MOUSE_WHEEL_UP = 0x78 + MOUSE_WHEEL_DOWN = 0xFF88 + class FilterMouseState(IntEnum): FILTER_MOUSE_NONE = 0x0000 @@ -61,15 +75,22 @@ class FilterMouseState(IntEnum): FILTER_MOUSE_MOVE = 0x1000 -class MouseFlag(IntEnum): - MOUSE_MOVE_RELATIVE = 0x000 - MOUSE_MOVE_ABSOLUTE = 0x001 - MOUSE_VIRTUAL_DESKTOP = 0x002 - MOUSE_ATTRIBUTES_CHANGED = 0x004 - MOUSE_MOVE_NOCOALESCE = 0x008 - MOUSE_TERMSRV_SRC_SHADOW = 0x100 +class FilterKeyState(IntEnum): + FILTER_KEY_NONE = 0x0000 + FILTER_KEY_ALL = 0xFFFF + FILTER_KEY_DOWN = KeyState.KEY_UP + FILTER_KEY_UP = KeyState.KEY_UP << 1 + FILTER_KEY_E0 = KeyState.KEY_E0 << 1 + FILTER_KEY_E1 = KeyState.KEY_E1 << 1 + FILTER_KEY_TERMSRV_SET_LED = KeyState.KEY_TERMSRV_SET_LED << 1 + FILTER_KEY_TERMSRV_SHADOW = KeyState.KEY_TERMSRV_SHADOW << 1 + FILTER_KEY_TERMSRV_VKPACKET = KeyState.KEY_TERMSRV_VKPACKET << 1 -class MouseRolling(IntEnum): - MOUSE_WHEEL_UP = 0x78 - MOUSE_WHEEL_DOWN = 0xFF88 +_MAPPED_MOUSE_BUTTONS = { + "left": (MouseState.MOUSE_LEFT_BUTTON_DOWN, MouseState.MOUSE_LEFT_BUTTON_UP), + "right": (MouseState.MOUSE_RIGHT_BUTTON_DOWN, MouseState.MOUSE_RIGHT_BUTTON_UP), + "middle": (MouseState.MOUSE_MIDDLE_BUTTON_DOWN, MouseState.MOUSE_MIDDLE_BUTTON_UP), + "mouse4": (MouseState.MOUSE_BUTTON_4_DOWN, MouseState.MOUSE_BUTTON_4_UP), + "mouse5": (MouseState.MOUSE_BUTTON_5_DOWN, MouseState.MOUSE_BUTTON_5_UP), +} diff --git a/src/interception/_utils.py b/src/interception/_utils.py new file mode 100644 index 0000000..16a7432 --- /dev/null +++ b/src/interception/_utils.py @@ -0,0 +1,56 @@ +import math +from typing import Optional + +import win32api # type: ignore + + +def normalize(x: int | tuple[int, int], y: Optional[int] = None) -> tuple[int, int]: + """Normalizes an x, y position to allow passing them seperately or as tuple.""" + if isinstance(x, tuple): + if len(x) == 2: + x, y = x + elif len(x) == 4: + x, y, *_ = x + else: + raise ValueError(f"Cant normalize tuple of length {len(x)}: {x}") + else: + assert y is not None + + return int(x), int(y) + + +def to_interception_coordinate(x: int, y: int) -> tuple[int, int]: + """Scales a "normal" coordinate to the respective point in the interception + coordinate system. + + The interception coordinate system covers all 16-bit unsigned integers, + ranging from `0x0` to `0xFFFF (65535)`. + + To arrive at the formula, we first have to realize the following: + - The maximum value in the 16-bit system is so `0xFFFF (~65535)` + - The maximum value, depending on your monitor, would for example be `1920` + - To calculate the factor, we can calculate `65535 / 1920 = ~34.13`. + - Thus we found out, that `scaled x = factor * original x` and `factor = 0xFFFF / axis` + + So, to bring it to code: + ```py + xfactor = 0xFFFF / screen_width + yfactor = 0xFFFF / screen_height + ``` + + Now, using that factor, we can calculate the position of our coordinate as such: + ```py + interception_x = round(xfactor * x) + interception_y = round(yfactor * y) + """ + + def scale(dimension: int, point: int) -> int: + return int((0xFFFF / dimension) * point) + 1 + + screen = win32api.GetSystemMetrics(0), win32api.GetSystemMetrics(1) + return scale(screen[0], x), scale(screen[1], y) + + +def get_cursor_pos() -> tuple[int, int]: + """Gets the current position of the cursor using `GetCursorPos`""" + return win32api.GetCursorPos() diff --git a/src/interception/exceptions.py b/src/interception/exceptions.py new file mode 100644 index 0000000..2af85b5 --- /dev/null +++ b/src/interception/exceptions.py @@ -0,0 +1,21 @@ +class InterceptionNotInstalled(Exception): + """Raised when the interception driver is not installed.""" + + +class InvalidKeyRequested(LookupError): + """Raised when attemping to press a key that doesnt exist""" + + def __init__(self, key: str) -> None: + self.key = key + + def __str__(self) -> str: + return f"Unsupported key requested: {self.key}" + +class InvalidMouseButtonRequested(LookupError): + """Raised when attemping to press a mouse button that doesnt exist""" + + def __init__(self, button: str) -> None: + self.button = button + + def __str__(self) -> str: + return f"Unsupported button requested: {self.button}" \ No newline at end of file diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 371364c..c284de8 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -1,199 +1,67 @@ -import ctypes +import functools import time from contextlib import contextmanager -from ctypes import wintypes from typing import Literal, Optional -from win32api import GetSystemMetrics # type:ignore[import] - +from . import _utils from ._consts import * from ._keycodes import KEYBOARD_MAPPING +from . import exceptions from .interception import Interception from .strokes import KeyStroke, MouseStroke +from .types import MouseButton +# try to initialize interception, if it fails simply remember that it failed to initalize. +# I want to avoid raising the error on import and instead raise it when attempting to call +# functioality that relies on the driver, this also still allows access to non driver functionality +# such as `mouse_position()` try: interception = Interception() + INTERCEPTION_INSTALLED = True except Exception: - print("Failed to initialize Interception.") - -_screen_width = GetSystemMetrics(0) -_screen_height = GetSystemMetrics(1) + INTERCEPTION_INSTALLED = False keyboard = 1 mouse = 11 -MOUSE_BUTTON_MAPPING = { - "left": (MouseState.MOUSE_LEFT_BUTTON_DOWN, MouseState.MOUSE_LEFT_BUTTON_UP), - "right": (MouseState.MOUSE_RIGHT_BUTTON_DOWN, MouseState.MOUSE_RIGHT_BUTTON_UP), - "middle": (MouseState.MOUSE_MIDDLE_BUTTON_DOWN, MouseState.MOUSE_MIDDLE_BUTTON_UP), - "mouse4": (MouseState.MOUSE_BUTTON_4_DOWN, MouseState.MOUSE_BUTTON_4_UP), - "mouse5": (MouseState.MOUSE_BUTTON_5_DOWN, MouseState.MOUSE_BUTTON_5_UP), -} - - -def _normalize(x: int | tuple[int, int], y: Optional[int] = None) -> tuple[int, int]: - """Normalizes an x, y position to allow passing them seperately or as tuple.""" - if isinstance(x, tuple): - if len(x) == 2: - x, y = x - elif len(x) == 4: - x, y, *_ = x - else: - raise ValueError(f"Cant normalize tuple of length {len(x)}: {x}") - else: - assert y is not None - - return int(x), int(y) - - -def _to_hexadecimal(screen_size: int, i: int): - """Converts a coordinate from the usual system to a coordinate interception - can understand and move to.""" - return int((0xFFFF / screen_size) * i) + 1 - - -def _to_interception_point(x: int, y: int) -> tuple[int, int]: - """Converts a point (x, y) from the usual system to a point interception - can understand and move to.""" - return ( - _to_hexadecimal(_screen_width, x), - _to_hexadecimal(_screen_height, y), - ) - - -def _get_cursor_pos() -> tuple[int, int]: - """Gets the current position of the cursor using `GetCursorPos`""" - cursor = wintypes.POINT() - ctypes.windll.user32.GetCursorPos(ctypes.byref(cursor)) - return (int(cursor.x), int(cursor.y)) - - -def capture_keyboard() -> int: - """Captures keyboard keypresses until the `Escape` key is pressed. - Filters out non `KEY_DOWN` events to not post the same capture twice. - """ - context = Interception() - context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) +def requires_driver(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + if not INTERCEPTION_INSTALLED: + raise exceptions.InterceptionNotInstalled + return func(*args, **kwargs) - print("Capturing keyboard presses, press ESC to quit.") - try: - while True: - device = context.wait() - stroke = context.receive(device) - - if stroke.code == 0x01: - print("ESC pressed, exited.") - return device - - print(f"Received stroke {stroke} on keyboard device {device}") - context.send(device, stroke) - finally: - context._destroy_context() - - -def capture_mouse() -> int: - """Captures mouse left clicks until the `Escape` key is pressed. - - Filters out non `LEFT_BUTTON_DOWN` events to not post the same capture twice. - """ - context = Interception() - context.set_filter(context.is_mouse, FilterMouseState.FILTER_MOUSE_LEFT_BUTTON_DOWN) - context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) - - print("Capturing mouse left clicks, press ESC to quit.") - try: - while True: - device = context.wait() - stroke = context.receive(device) - - if context.is_keyboard(device) and stroke.code == 0x01: - print("ESC pressed, exited.") - return device - - elif not context.is_keyboard(device): - print(f"Received stroke {stroke} on mouse device {device}") - - context.send(device, stroke) - finally: - context._destroy_context() - - -def listen_to_keyboard() -> int: - """Captures keyboard keypresses until the `Escape` key is pressed. - - Doesn't filter out any events at all. - """ - context = Interception() - context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_ALL) - - print("Listenting to keyboard, press ESC to quit.") - try: - while True: - device = context.wait() - stroke = context.receive(device) - - if stroke.code == 0x01: - print("ESC pressed, exited.") - return device - - print(f"Received stroke {stroke} on keyboard device {device}") - context.send(device, stroke) - finally: - context._destroy_context() - - -def listen_to_mouse() -> int: - """Captures mouse movements / clicks until the `Escape` key is pressed. - - Doesn't filter out any events at all. - """ - context = Interception() - context.set_filter(context.is_mouse, FilterMouseState.FILTER_MOUSE_ALL) - context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) - - print("Listenting to mouse, press ESC to quit.") - try: - while True: - device = context.wait() - stroke = context.receive(device) - - if context.is_keyboard(device) and stroke.code == 0x01: - print("ESC pressed, exited.") - return device - - elif not context.is_keyboard(device): - print(f"Received stroke {stroke} on mouse device {device}") - - context.send(device, stroke) - finally: - context._destroy_context() + return wrapper +@requires_driver def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: """Moves to a given position.""" - x, y = _normalize(x, y) - x, y = _to_interception_point(x, y) + x, y = _utils.normalize(x, y) + x, y = _utils.to_interception_coordinate(x, y) stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, x, y, 0) interception.send(mouse, stroke) +@requires_driver def move_relative(x: int = 0, y: int = 0) -> None: """Moves relatively to a given position.""" - curr = _get_cursor_pos() + curr = _utils.get_cursor_pos() move_to(curr[0] + x, curr[1] + y) -def position() -> tuple[int, int]: +def mouse_position() -> tuple[int, int]: """Returns the position of the cursor on the monitor.""" - return _get_cursor_pos() + return _utils.get_cursor_pos() +@requires_driver def click( x: Optional[int | tuple[int, int]] = None, y: Optional[int] = None, - button: Literal["left", "right", "middle", "mouse4", "mouse5"] = "left", + button: MouseButton = "left", clicks: int = 1, interval: int | float = 0.1, delay: int | float = 0.3, @@ -227,16 +95,19 @@ def click( time.sleep(interval) +@requires_driver def left_click(clicks: int = 1, interval: int | float = 0.1) -> None: """Left clicks at the current position.""" click(button="left", clicks=clicks, interval=interval) +@requires_driver def right_click(clicks: int = 1, interval: int | float = 0.1) -> None: """Right cicks at the current position.""" click(button="right", clicks=clicks, interval=interval) +@requires_driver def press(key: str, presses: int = 1, interval: int | float = 0.1) -> None: """Presses a key. @@ -256,11 +127,12 @@ def press(key: str, presses: int = 1, interval: int | float = 0.1) -> None: key_down(key) key_up(key) except KeyError: - raise ValueError(f"Unsupported key, {key}") + raise exceptions.InvalidKeyRequested(key) if presses > 1: time.sleep(interval) +@requires_driver def write(term: str, interval: int | float = 0.05) -> None: """Writes a term by sending each key one after another. @@ -277,6 +149,7 @@ def write(term: str, interval: int | float = 0.05) -> None: time.sleep(interval) +@requires_driver def scroll(direction: Literal["up", "down"]) -> None: """Scrolls the mouse wheel one unit in a given direction.""" amount = ( @@ -292,6 +165,7 @@ def scroll(direction: Literal["up", "down"]) -> None: time.sleep(0.025) +@requires_driver def key_down(key: str) -> None: """Holds a key down, will not be released automatically. @@ -303,6 +177,7 @@ def key_down(key: str) -> None: time.sleep(0.025) +@requires_driver def key_up(key: str) -> None: """Releases a key.""" stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_UP, 0) @@ -310,30 +185,33 @@ def key_up(key: str) -> None: time.sleep(0.025) -def mouse_down(button: Literal["left", "right", "middle", "mouse4", "mouse5"]) -> None: +@requires_driver +def mouse_down(button: MouseButton, delay: int | float = 0.03) -> None: """Holds a mouse button down, will not be released automatically. If you want to hold a mouse button while performing an action, please use `hold_mouse`, which offers a context manager. """ - down, _ = MOUSE_BUTTON_MAPPING[button] + down, _ = MouseState.from_string(button) stroke = MouseStroke(down, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) interception.send(mouse, stroke) - time.sleep(0.03) + time.sleep(delay) -def mouse_up(button: Literal["left", "right", "middle", "mouse4", "mouse5"]) -> None: +@requires_driver +def mouse_up(button: MouseButton) -> None: """Releases a mouse button.""" - _, up = MOUSE_BUTTON_MAPPING[button] + _, up = MouseState.from_string(button) stroke = MouseStroke(up, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) interception.send(mouse, stroke) time.sleep(0.03) +@requires_driver @contextmanager -def hold_mouse(button: Literal["left", "right", "middle", "mouse4", "mouse5"]): +def hold_mouse(button: MouseButton): """A context manager to hold a mouse button while performing another action. Example: @@ -348,6 +226,7 @@ def hold_mouse(button: Literal["left", "right", "middle", "mouse4", "mouse5"]): mouse_up(button=button) +@requires_driver @contextmanager def hold_key(key: str): """A context manager to hold a mouse button while performing another action. @@ -362,3 +241,109 @@ def hold_key(key: str): yield finally: key_up(key) + + +@requires_driver +def capture_keyboard() -> int: + """Captures keyboard keypresses until the `Escape` key is pressed. + + Filters out non `KEY_DOWN` events to not post the same capture twice. + """ + context = Interception() + context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) + + print("Capturing keyboard presses, press ESC to quit.") + try: + while True: + device = context.wait() + stroke = context.receive(device) + + if stroke.code == 0x01: + print("ESC pressed, exited.") + return device + + print(f"Received stroke {stroke} on keyboard device {device}") + context.send(device, stroke) + finally: + context._destroy_context() + + +@requires_driver +def capture_mouse() -> int: + """Captures mouse left clicks until the `Escape` key is pressed. + + Filters out non `LEFT_BUTTON_DOWN` events to not post the same capture twice. + """ + context = Interception() + context.set_filter(context.is_mouse, FilterMouseState.FILTER_MOUSE_LEFT_BUTTON_DOWN) + context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) + + print("Capturing mouse left clicks, press ESC to quit.") + try: + while True: + device = context.wait() + stroke = context.receive(device) + + if context.is_keyboard(device) and stroke.code == 0x01: + print("ESC pressed, exited.") + return device + + elif not context.is_keyboard(device): + print(f"Received stroke {stroke} on mouse device {device}") + + context.send(device, stroke) + finally: + context._destroy_context() + + +@requires_driver +def listen_to_keyboard() -> int: + """Captures keyboard keypresses until the `Escape` key is pressed. + + Doesn't filter out any events at all. + """ + context = Interception() + context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_ALL) + + print("Listenting to keyboard, press ESC to quit.") + try: + while True: + device = context.wait() + stroke = context.receive(device) + + if stroke.code == 0x01: + print("ESC pressed, exited.") + return device + + print(f"Received stroke {stroke} on keyboard device {device}") + context.send(device, stroke) + finally: + context._destroy_context() + + +@requires_driver +def listen_to_mouse() -> int: + """Captures mouse movements / clicks until the `Escape` key is pressed. + + Doesn't filter out any events at all. + """ + context = Interception() + context.set_filter(context.is_mouse, FilterMouseState.FILTER_MOUSE_ALL) + context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) + + print("Listenting to mouse, press ESC to quit.") + try: + while True: + device = context.wait() + stroke = context.receive(device) + + if context.is_keyboard(device) and stroke.code == 0x01: + print("ESC pressed, exited.") + return device + + elif not context.is_keyboard(device): + print(f"Received stroke {stroke} on mouse device {device}") + + context.send(device, stroke) + finally: + context._destroy_context() diff --git a/src/interception/types.py b/src/interception/types.py new file mode 100644 index 0000000..51a4319 --- /dev/null +++ b/src/interception/types.py @@ -0,0 +1,3 @@ +from typing import Literal + +MouseButton = Literal["left", "right", "middle", "mouse4", "mouse5"] From 8f50c518e8afb4cc860de042d5d8c54a93a7a992 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Mon, 22 May 2023 16:57:42 +0200 Subject: [PATCH 21/37] added global delays --- src/interception/inputs.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/interception/inputs.py b/src/interception/inputs.py index c284de8..9f09184 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -21,6 +21,10 @@ except Exception: INTERCEPTION_INSTALLED = False + +MOUSE_BUTTON_DELAY = 0.03 +KEY_PRESS_DELAY = 0.025 + keyboard = 1 mouse = 11 @@ -166,7 +170,7 @@ def scroll(direction: Literal["up", "down"]) -> None: @requires_driver -def key_down(key: str) -> None: +def key_down(key: str, delay: Optional[float] = None) -> None: """Holds a key down, will not be released automatically. If you want to hold a key while performing an action, please use @@ -174,19 +178,20 @@ def key_down(key: str) -> None: """ stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_DOWN, 0) interception.send(keyboard, stroke) - time.sleep(0.025) + + time.sleep(delay or KEY_PRESS_DELAY) @requires_driver -def key_up(key: str) -> None: +def key_up(key: str, delay: Optional[float] = None) -> None: """Releases a key.""" stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_UP, 0) interception.send(keyboard, stroke) - time.sleep(0.025) + time.sleep(delay or KEY_PRESS_DELAY) @requires_driver -def mouse_down(button: MouseButton, delay: int | float = 0.03) -> None: +def mouse_down(button: MouseButton, delay: Optional[float] = None) -> None: """Holds a mouse button down, will not be released automatically. If you want to hold a mouse button while performing an action, please use @@ -196,17 +201,17 @@ def mouse_down(button: MouseButton, delay: int | float = 0.03) -> None: stroke = MouseStroke(down, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) interception.send(mouse, stroke) - time.sleep(delay) + time.sleep(delay or MOUSE_BUTTON_DELAY) @requires_driver -def mouse_up(button: MouseButton) -> None: +def mouse_up(button: MouseButton, delay: Optional[float] = None) -> None: """Releases a mouse button.""" _, up = MouseState.from_string(button) stroke = MouseStroke(up, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) interception.send(mouse, stroke) - time.sleep(0.03) + time.sleep(delay or MOUSE_BUTTON_DELAY) @requires_driver From 903a03690c1b86b375f398460818365fbbb0f914 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Sat, 27 May 2023 12:15:54 +0200 Subject: [PATCH 22/37] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b90f7e3..ba48894 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# pyintercept +# pyinterception This is a greatly reworked fork of [interception_py][wrp], a python port for [interception][c_ception]. The Interception API aims to build a portable programming interface that allows one to intercept and control a range of input devices. From e3dfd1cbbb3719f037d1d0062e95f08b33d0a620 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Sat, 27 May 2023 12:16:49 +0200 Subject: [PATCH 23/37] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ba48894..f19141f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # pyinterception -This is a greatly reworked fork of [interception_py][wrp], a python port for [interception][c_ception]. +This is a greatly reworked version of [interception_py][wrp], a python port for [interception][c_ception], which is now obsolete and points here instead. The Interception API aims to build a portable programming interface that allows one to intercept and control a range of input devices. From d49ab5f70aa8ea53b28282ef354490683860bee4 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Sat, 27 May 2023 12:17:47 +0200 Subject: [PATCH 24/37] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f19141f..47d3b75 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ which made some things pretty hard to decipher and understand. ## How to use? -First of all, you absolutely need to install the [interception-driver][c_ception], otherwise none of this will work. Dont worry, is's as simple as running a .bat file and restarting your computer. +First of all, you absolutely need to install the [interception-driver][c_ception], otherwise none of this will work. It's a very simple install. Now, once you have all of that set up, you can go ahead and import `interception`. Let's start by identifying your used devices! From e5156f291494ee20e13d1b89f46c902c8bf72f17 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Sat, 27 May 2023 12:56:17 +0200 Subject: [PATCH 25/37] fixed scrolling --- src/interception/inputs.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 9f09184..7f5bae1 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -153,7 +153,6 @@ def write(term: str, interval: int | float = 0.05) -> None: time.sleep(interval) -@requires_driver def scroll(direction: Literal["up", "down"]) -> None: """Scrolls the mouse wheel one unit in a given direction.""" amount = ( @@ -162,9 +161,7 @@ def scroll(direction: Literal["up", "down"]) -> None: else MouseRolling.MOUSE_WHEEL_DOWN ) - stroke = MouseStroke( - MouseState.MOUSE_WHEEL, MouseFlag.MOUSE_MOVE_RELATIVE, amount, 0, 0, 0 - ) + stroke = MouseStroke(MouseState.MOUSE_WHEEL, 0, amount, 0, 0, 0) interception.send(mouse, stroke) time.sleep(0.025) @@ -178,7 +175,7 @@ def key_down(key: str, delay: Optional[float] = None) -> None: """ stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_DOWN, 0) interception.send(keyboard, stroke) - + time.sleep(delay or KEY_PRESS_DELAY) From 1efb8ca3c3f7074290b1d13bf7fb31e005d21cc2 Mon Sep 17 00:00:00 2001 From: David Morgen <128868063+dvdco@users.noreply.github.com> Date: Thu, 1 Jun 2023 18:16:18 +0800 Subject: [PATCH 26/37] Add files via upload corrected the MouseFlag.MOUSE_MOVE_RELATIVE in _consts.py updated the inputs.py to use real relative move, rather than getting the mouse position, add delta and abs move now can work better in fps games --- src/interception/_consts.py | 2 +- src/interception/inputs.py | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/interception/_consts.py b/src/interception/_consts.py index ec9dafd..2797086 100644 --- a/src/interception/_consts.py +++ b/src/interception/_consts.py @@ -41,7 +41,7 @@ def from_string( class MouseFlag(IntEnum): - MOUSE_MOVE_RELATIVE = 0x000F + MOUSE_MOVE_RELATIVE = 0x000 MOUSE_MOVE_ABSOLUTE = 0x001 MOUSE_VIRTUAL_DESKTOP = 0x002 MOUSE_ATTRIBUTES_CHANGED = 0x004 diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 7f5bae1..a2d9c8a 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -44,16 +44,18 @@ def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: """Moves to a given position.""" x, y = _utils.normalize(x, y) x, y = _utils.to_interception_coordinate(x, y) - stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, x, y, 0) interception.send(mouse, stroke) @requires_driver -def move_relative(x: int = 0, y: int = 0) -> None: - """Moves relatively to a given position.""" - curr = _utils.get_cursor_pos() - move_to(curr[0] + x, curr[1] + y) +def move_relative(x: int | tuple[int, int], y: Optional[int] = None) -> None: + """Moves to a given position.""" + x, y = _utils.normalize(x, y) + # x, y = _utils.to_interception_coordinate(x, y) + + stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_RELATIVE, 0, x, y, 0) + interception.send(mouse, stroke) def mouse_position() -> tuple[int, int]: @@ -153,6 +155,7 @@ def write(term: str, interval: int | float = 0.05) -> None: time.sleep(interval) +@requires_driver def scroll(direction: Literal["up", "down"]) -> None: """Scrolls the mouse wheel one unit in a given direction.""" amount = ( @@ -161,7 +164,9 @@ def scroll(direction: Literal["up", "down"]) -> None: else MouseRolling.MOUSE_WHEEL_DOWN ) - stroke = MouseStroke(MouseState.MOUSE_WHEEL, 0, amount, 0, 0, 0) + stroke = MouseStroke( + MouseState.MOUSE_WHEEL, MouseFlag.MOUSE_MOVE_RELATIVE, amount, 0, 0, 0 + ) interception.send(mouse, stroke) time.sleep(0.025) @@ -175,7 +180,7 @@ def key_down(key: str, delay: Optional[float] = None) -> None: """ stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_DOWN, 0) interception.send(keyboard, stroke) - + time.sleep(delay or KEY_PRESS_DELAY) From a598a14e4c7b815a85d69c008bfac9aab5f44184 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Thu, 1 Jun 2023 14:11:52 +0200 Subject: [PATCH 27/37] finishing touches --- src/interception/inputs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interception/inputs.py b/src/interception/inputs.py index a2d9c8a..40797a6 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -44,16 +44,16 @@ def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: """Moves to a given position.""" x, y = _utils.normalize(x, y) x, y = _utils.to_interception_coordinate(x, y) + stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, x, y, 0) interception.send(mouse, stroke) @requires_driver def move_relative(x: int | tuple[int, int], y: Optional[int] = None) -> None: - """Moves to a given position.""" + """Moves the cursor by a given x and y amount.""" x, y = _utils.normalize(x, y) - # x, y = _utils.to_interception_coordinate(x, y) - + stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_RELATIVE, 0, x, y, 0) interception.send(mouse, stroke) From 8eaf2e52c9dae63196f5f3fbd1e5f4fdcb742cba Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Thu, 1 Jun 2023 14:15:27 +0200 Subject: [PATCH 28/37] changed scroll function back --- src/interception/inputs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 40797a6..6725728 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -164,9 +164,7 @@ def scroll(direction: Literal["up", "down"]) -> None: else MouseRolling.MOUSE_WHEEL_DOWN ) - stroke = MouseStroke( - MouseState.MOUSE_WHEEL, MouseFlag.MOUSE_MOVE_RELATIVE, amount, 0, 0, 0 - ) + stroke = MouseStroke(MouseState.MOUSE_WHEEL, 0, amount, 0, 0, 0) interception.send(mouse, stroke) time.sleep(0.025) From 069e003341e48eeae45351878bad71ea4aa01fad Mon Sep 17 00:00:00 2001 From: southwestfrance <72102954+southwestfrance@users.noreply.github.com> Date: Fri, 2 Jun 2023 19:33:20 -0700 Subject: [PATCH 29/37] Update README.md key_down should be changed to hold_key in the example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 47d3b75..2c0f4d5 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ So, now you can begin to send inputs, just like you are used to it from pyautogu ```py interception.move_to(960, 540) -with interception.key_down("shift"): +with interception.hold_key("shift"): interception.press("a") interception.click(120, 160, button="right", delay=1) From 34f05db46cf6f14ac4233a667c630641ec950718 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Sun, 13 Aug 2023 13:04:25 +0200 Subject: [PATCH 30/37] improved documentation of move_to function --- src/interception/_keycodes.py | 2 +- src/interception/exceptions.py | 15 +++++++--- src/interception/inputs.py | 52 +++++++++++++++++++++++++++------- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/interception/_keycodes.py b/src/interception/_keycodes.py index 9cf5a13..44498ef 100644 --- a/src/interception/_keycodes.py +++ b/src/interception/_keycodes.py @@ -109,4 +109,4 @@ } for c in range(32, 92): - KEYBOARD_MAPPING[chr(c).lower()] = win32api.MapVirtualKey(c, 0) + KEYBOARD_MAPPING[chr(c).lower()] = win32api.MapVirtualKey(c, 0) \ No newline at end of file diff --git a/src/interception/exceptions.py b/src/interception/exceptions.py index 2af85b5..3bc76c2 100644 --- a/src/interception/exceptions.py +++ b/src/interception/exceptions.py @@ -1,5 +1,11 @@ -class InterceptionNotInstalled(Exception): - """Raised when the interception driver is not installed.""" +class DriverNotFoundError(Exception): + """Raised when the interception driver is not installed / found.""" + + def __str__(self) -> str: + return ( + "Interception driver was not found or is not installed.\n" + "Please confirm that it has been installed properly and is added to PATH." + ) class InvalidKeyRequested(LookupError): @@ -10,7 +16,8 @@ def __init__(self, key: str) -> None: def __str__(self) -> str: return f"Unsupported key requested: {self.key}" - + + class InvalidMouseButtonRequested(LookupError): """Raised when attemping to press a mouse button that doesnt exist""" @@ -18,4 +25,4 @@ def __init__(self, button: str) -> None: self.button = button def __str__(self) -> str: - return f"Unsupported button requested: {self.button}" \ No newline at end of file + return f"Unsupported button requested: {self.button}" diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 6725728..5b8c361 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -3,18 +3,17 @@ from contextlib import contextmanager from typing import Literal, Optional -from . import _utils -from ._consts import * +from . import _utils, exceptions +from ._consts import (FilterKeyState, FilterMouseState, KeyState, MouseFlag, + MouseRolling, MouseState) from ._keycodes import KEYBOARD_MAPPING -from . import exceptions from .interception import Interception from .strokes import KeyStroke, MouseStroke from .types import MouseButton # try to initialize interception, if it fails simply remember that it failed to initalize. # I want to avoid raising the error on import and instead raise it when attempting to call -# functioality that relies on the driver, this also still allows access to non driver functionality -# such as `mouse_position()` +# functionality that relies on the driver, this also still allows access to non driver stuff try: interception = Interception() INTERCEPTION_INSTALLED = True @@ -30,10 +29,12 @@ def requires_driver(func): + """Wraps any function that requires the interception driver to be installed + such that, if it is not installed, a `DriverNotFoundError` is raised""" @functools.wraps(func) def wrapper(*args, **kwargs): if not INTERCEPTION_INSTALLED: - raise exceptions.InterceptionNotInstalled + raise exceptions.DriverNotFoundError return func(*args, **kwargs) return wrapper @@ -41,19 +42,48 @@ def wrapper(*args, **kwargs): @requires_driver def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: - """Moves to a given position.""" + """Moves to a given absolute (x, y) location on the screen. + + The paramters can be passed as a tuple-like `(x, y)` coordinate or + seperately as `x` and `y` coordinates, it will be parsed accordingly. + + Due to conversion to the coordinate system the interception driver + uses, an offset of 1 pixel in either x or y axis may occur or not. + + ### Examples: + ```py + # passing x and y seperately, typical when manually calling the function + interception.move_to(800, 1200) + + # passing a tuple-like coordinate, typical for dynamic operations. + # simply avoids having to unpack the arguments. + target_location = (1200, 300) + interception.move_to(target_location) + ``` + """ x, y = _utils.normalize(x, y) x, y = _utils.to_interception_coordinate(x, y) - + stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, x, y, 0) interception.send(mouse, stroke) @requires_driver def move_relative(x: int | tuple[int, int], y: Optional[int] = None) -> None: - """Moves the cursor by a given x and y amount.""" - x, y = _utils.normalize(x, y) + """Moves relatively from the current cursor position by the given amounts. + + The paramters can be passed as tuple-like `(x, y)` amouints or + seperately as `x` and `y` amount, it will be parsed accordingly. + + Due to conversion to the coordinate system the interception driver + uses, an offset of 1 pixel in either x or y axis may occur or not. + + + + """ + x, y = _utils.normalize(x, y) + stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_RELATIVE, 0, x, y, 0) interception.send(mouse, stroke) @@ -178,7 +208,7 @@ def key_down(key: str, delay: Optional[float] = None) -> None: """ stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_DOWN, 0) interception.send(keyboard, stroke) - + time.sleep(delay or KEY_PRESS_DELAY) From 2198fa1520aff388432a2da2978549fad55059bb Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Sun, 13 Aug 2023 17:25:29 +0200 Subject: [PATCH 31/37] fixed keycodes mapped inproperly --- src/interception/_consts.py | 10 +- src/interception/_keycodes.py | 236 ++++++++++++++++++--------------- src/interception/_utils.py | 1 - src/interception/exceptions.py | 8 +- src/interception/inputs.py | 110 ++++++++++----- src/interception/types.py | 2 +- 6 files changed, 209 insertions(+), 158 deletions(-) diff --git a/src/interception/_consts.py b/src/interception/_consts.py index 2797086..71f0f92 100644 --- a/src/interception/_consts.py +++ b/src/interception/_consts.py @@ -1,7 +1,6 @@ from __future__ import annotations from enum import IntEnum from typing import Literal -from .exceptions import InvalidMouseButtonRequested class KeyState(IntEnum): @@ -31,13 +30,8 @@ class MouseState(IntEnum): MOUSE_HWHEEL = 0x800 @staticmethod - def from_string( - button: Literal["left", "right", "middle", "mouse4", "mouse5"] - ) -> tuple[MouseState, MouseState]: - try: - return _MAPPED_MOUSE_BUTTONS[button] - except KeyError: - raise InvalidMouseButtonRequested(button) + def from_string(button: str) -> tuple[MouseState, MouseState]: + return _MAPPED_MOUSE_BUTTONS[button] class MouseFlag(IntEnum): diff --git a/src/interception/_keycodes.py b/src/interception/_keycodes.py index 44498ef..edcda0f 100644 --- a/src/interception/_keycodes.py +++ b/src/interception/_keycodes.py @@ -1,112 +1,132 @@ -import ctypes -import win32api # type: ignore[import] +from ctypes import windll, wintypes KEYBOARD_MAPPING = { - "escape": 0x01, - "esc": 0x01, - "f1": 0x3B, - "f2": 0x3C, - "f3": 0x3D, - "f4": 0x3E, - "f5": 0x3F, - "f6": 0x40, - "f7": 0x41, - "f8": 0x42, - "f9": 0x43, - "f10": 0x44, - "f11": 0x57, - "f12": 0x58, - "printscreen": 0xB7, - "prntscrn": 0xB7, - "prtsc": 0xB7, - "prtscr": 0xB7, - "scrolllock": 0x46, - "pause": 0xC5, - "`": 0x29, - "1": 0x02, - "2": 0x03, - "3": 0x04, - "4": 0x05, - "5": 0x06, - "6": 0x07, - "7": 0x08, - "8": 0x09, - "9": 0x0A, - "0": 0x0B, - "-": 0x0C, - "=": 0x0D, - "backspace": 0x0E, - "insert": 0xD2 + 1024, - "home": 0xC7 + 1024, - "pageup": 0xC9 + 1024, - "pagedown": 0xD1 + 1024, - "numlock": 0x45, - "divide": 0xB5 + 1024, - "multiply": 0x37, - "subtract": 0x4A, - "add": 0x4E, - "decimal": 0x53, - "tab": 0x0F, - "q": 0x10, - "w": 0x11, - "e": 0x12, - "r": 0x13, - "t": 0x14, - "y": 0x2C, - "u": 0x16, - "i": 0x17, - "o": 0x18, - "p": 0x19, - "[": 0x1A, - "]": 0x1B, - "\\": 0x2B, - "del": 0xD3 + 1024, - "delete": 0xD3 + 1024, - "end": 0xCF + 1024, - "capslock": 0x3A, - "a": 0x1E, - "s": 0x1F, - "d": 0x20, - "f": 0x21, - "g": 0x22, - "h": 0x23, - "j": 0x24, - "k": 0x25, - "l": 0x26, - ";": 0x27, - "'": 0x28, - "enter": 0x1C, - "return": 0x1C, - "shift": 0x2A, - "shiftleft": 0x2A, - "z": 0x15, - "x": 0x2D, - "c": 0x2E, - "v": 0x2F, - "b": 0x30, - "n": 0x31, - "m": 0x32, - ",": 0x33, - ".": 0x34, - "/": 0x35, - "shiftright": 0x36, - "ctrl": 0x1D, - "ctrlleft": 0x1D, - "win": 0xDB + 1024, - "winleft": 0xDB + 1024, - "alt": 0x38, - "altleft": 0x38, - " ": 0x39, - "space": 0x39, - "altright": 0xB8 + 1024, - "winright": 0xDC + 1024, - "apps": 0xDD + 1024, - "ctrlright": 0x9D + 1024, - "up": ctypes.windll.user32.MapVirtualKeyW(0x26, 0), - "left": ctypes.windll.user32.MapVirtualKeyW(0x25, 0), - "down": ctypes.windll.user32.MapVirtualKeyW(0x28, 0), - "right": ctypes.windll.user32.MapVirtualKeyW(0x27, 0), + "backspace": 0x08, + "\b": 0x08, + "super": 0x5B, + "tab": 0x09, + "\t": 0x09, + "clear": 0x0C, + "enter": 0x0D, + "\n": 0x0D, + "return": 0x0D, + "shift": 0x10, + "ctrl": 0x11, + "alt": 0x12, + "pause": 0x13, + "capslock": 0x14, + "kana": 0x15, + "hanguel": 0x15, + "hangul": 0x15, + "junja": 0x17, + "final": 0x18, + "hanja": 0x19, + "kanji": 0x19, + "esc": 0x1B, + "escape": 0x1B, + "convert": 0x1C, + "nonconvert": 0x1D, + "accept": 0x1E, + "modechange": 0x1F, + " ": 0x20, + "space": 0x20, + "pgup": 0x21, + "pgdn": 0x22, + "pageup": 0x21, + "pagedown": 0x22, + "end": 0x23, + "home": 0x24, + "left": 0x25, + "up": 0x26, + "right": 0x27, + "down": 0x28, + "select": 0x29, + "print": 0x2A, + "execute": 0x2B, + "prtsc": 0x2C, + "prtscr": 0x2C, + "prntscrn": 0x2C, + "printscreen": 0x2C, + "insert": 0x2D, + "del": 0x2E, + "delete": 0x2E, + "help": 0x2F, + "win": 0x5B, + "winleft": 0x5B, + "winright": 0x5C, + "apps": 0x5D, + "sleep": 0x5F, + "num0": 0x60, + "num1": 0x61, + "num2": 0x62, + "num3": 0x63, + "num4": 0x64, + "num5": 0x65, + "num6": 0x66, + "num7": 0x67, + "num8": 0x68, + "num9": 0x69, + "multiply": 0x6A, + "add": 0x6B, + "separator": 0x6C, + "subtract": 0x6D, + "decimal": 0x6E, + "divide": 0x6F, + "f1": 0x70, + "f2": 0x71, + "f3": 0x72, + "f4": 0x73, + "f5": 0x74, + "f6": 0x75, + "f7": 0x76, + "f8": 0x77, + "f9": 0x78, + "f10": 0x79, + "f11": 0x7A, + "f12": 0x7B, + "f13": 0x7C, + "f14": 0x7D, + "f15": 0x7E, + "f16": 0x7F, + "f17": 0x80, + "f18": 0x81, + "f19": 0x82, + "f20": 0x83, + "f21": 0x84, + "f22": 0x85, + "f23": 0x86, + "f24": 0x87, + "numlock": 0x90, + "scrolllock": 0x91, + "shiftleft": 0xA0, + "shiftright": 0xA1, + "ctrlleft": 0xA2, + "ctrlright": 0xA3, + "altleft": 0xA4, + "altright": 0xA5, + "browserback": 0xA6, + "browserforward": 0xA7, + "browserrefresh": 0xA8, + "browserstop": 0xA9, + "browsersearch": 0xAA, + "browserfavorites": 0xAB, + "browserhome": 0xAC, + "volumemute": 0xAD, + "volumedown": 0xAE, + "volumeup": 0xAF, + "nexttrack": 0xB0, + "prevtrack": 0xB1, + "stop": 0xB2, + "playpause": 0xB3, + "launchmail": 0xB4, + "launchmediaselect": 0xB5, + "launchapp1": 0xB6, + "launchapp2": 0xB7, } -for c in range(32, 92): - KEYBOARD_MAPPING[chr(c).lower()] = win32api.MapVirtualKey(c, 0) \ No newline at end of file + +for c in range(32, 128): + KEYBOARD_MAPPING[chr(c).lower()] = windll.user32.VkKeyScanA(wintypes.WCHAR(chr(c))) + +for k, v in KEYBOARD_MAPPING.items(): + KEYBOARD_MAPPING[k] = windll.user32.MapVirtualKeyA(v, 0) \ No newline at end of file diff --git a/src/interception/_utils.py b/src/interception/_utils.py index 16a7432..ebba8b9 100644 --- a/src/interception/_utils.py +++ b/src/interception/_utils.py @@ -1,4 +1,3 @@ -import math from typing import Optional import win32api # type: ignore diff --git a/src/interception/exceptions.py b/src/interception/exceptions.py index 3bc76c2..f33e1b4 100644 --- a/src/interception/exceptions.py +++ b/src/interception/exceptions.py @@ -8,21 +8,21 @@ def __str__(self) -> str: ) -class InvalidKeyRequested(LookupError): +class UnknownKeyError(LookupError): """Raised when attemping to press a key that doesnt exist""" def __init__(self, key: str) -> None: self.key = key def __str__(self) -> str: - return f"Unsupported key requested: {self.key}" + return f"Unknown key requested: {self.key}" -class InvalidMouseButtonRequested(LookupError): +class UnknownButtonError(LookupError): """Raised when attemping to press a mouse button that doesnt exist""" def __init__(self, button: str) -> None: self.button = button def __str__(self) -> str: - return f"Unsupported button requested: {self.button}" + return f"Unknown button requested: {self.button}" diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 5b8c361..02a8c02 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -4,8 +4,14 @@ from typing import Literal, Optional from . import _utils, exceptions -from ._consts import (FilterKeyState, FilterMouseState, KeyState, MouseFlag, - MouseRolling, MouseState) +from ._consts import ( + FilterKeyState, + FilterMouseState, + KeyState, + MouseFlag, + MouseRolling, + MouseState, +) from ._keycodes import KEYBOARD_MAPPING from .interception import Interception from .strokes import KeyStroke, MouseStroke @@ -27,10 +33,14 @@ keyboard = 1 mouse = 11 +_SUPPORTED_BUTTONS = {"left", "right", "middle", "mouse4", "mouse5"} +_SUPPORTED_KEYS = dict(KEYBOARD_MAPPING) + def requires_driver(func): """Wraps any function that requires the interception driver to be installed such that, if it is not installed, a `DriverNotFoundError` is raised""" + @functools.wraps(func) def wrapper(*args, **kwargs): if not INTERCEPTION_INSTALLED: @@ -54,7 +64,7 @@ def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: ```py # passing x and y seperately, typical when manually calling the function interception.move_to(800, 1200) - + # passing a tuple-like coordinate, typical for dynamic operations. # simply avoids having to unpack the arguments. target_location = (1200, 300) @@ -69,27 +79,31 @@ def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: @requires_driver -def move_relative(x: int | tuple[int, int], y: Optional[int] = None) -> None: +def move_relative(x: int = 0, y: int = 0) -> None: """Moves relatively from the current cursor position by the given amounts. - The paramters can be passed as tuple-like `(x, y)` amouints or - seperately as `x` and `y` amount, it will be parsed accordingly. - Due to conversion to the coordinate system the interception driver uses, an offset of 1 pixel in either x or y axis may occur or not. - - + ### Example: + ```py + interception.mouse_position() + >>> 300, 400 + # move the mouse by 100 pixels on the x-axis and 0 in y-axis + interception.move_relative(100, 0) + interception.mouse_position() + >>> 400, 400 """ - x, y = _utils.normalize(x, y) - stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_RELATIVE, 0, x, y, 0) interception.send(mouse, stroke) def mouse_position() -> tuple[int, int]: - """Returns the position of the cursor on the monitor.""" + """Returns the current position of the cursor as `(x, y)` coordinate. + + This does nothing special like other conventional mouse position functions. + """ return _utils.get_cursor_pos() @@ -97,31 +111,31 @@ def mouse_position() -> tuple[int, int]: def click( x: Optional[int | tuple[int, int]] = None, y: Optional[int] = None, - button: MouseButton = "left", + button: MouseButton | str = "left", clicks: int = 1, interval: int | float = 0.1, delay: int | float = 0.3, ) -> None: - """Clicks at a given position. + """Presses a mouse button at a specific location (if given). Parameters ---------- - button :class:`Literal["left", "right", "middle", "mouse4", "mouse5"]`: + button :class:`Literal["left", "right", "middle", "mouse4", "mouse5"] | str`: The button to click once moved to the location (if passed), default "left". clicks :class:`int`: - The amount of mouse clicks to perform with the given button + The amount of mouse clicks to perform with the given button, default 1. interval :class:`int | float`: - The interval between multiple clicks, only applies if clicks > 1 + The interval between multiple clicks, only applies if clicks > 1, default 0.1. delay :class:`int | float`: - The delay between moving and clicking. + The delay between moving and clicking, default 0.3. """ + _check_button_exists(button) if x is not None: move_to(x, y) - - time.sleep(delay) + time.sleep(delay) for _ in range(clicks): mouse_down(button) @@ -131,15 +145,19 @@ def click( time.sleep(interval) +# decided against using functools.partial for left_click and right_click +# because it makes it less clear that the method attribute is a function +# and might be misunderstood. It also still allows changing the button +# argument afterall - just adds the correct default. @requires_driver def left_click(clicks: int = 1, interval: int | float = 0.1) -> None: - """Left clicks at the current position.""" + """Thin wrapper for the `click` function with the left mouse button.""" click(button="left", clicks=clicks, interval=interval) @requires_driver def right_click(clicks: int = 1, interval: int | float = 0.1) -> None: - """Right cicks at the current position.""" + """Thin wrapper for the `click` function with the right mouse button.""" click(button="right", clicks=clicks, interval=interval) @@ -150,20 +168,20 @@ def press(key: str, presses: int = 1, interval: int | float = 0.1) -> None: Parameters ---------- key :class:`str`: - The key to press. + The key to press, not case sensitive. presses :class:`int`: - The amount of presses to perform with the given key. + The amount of presses to perform with the given key, default 1. interval :class:`int | float`: - The interval between multiple presses, only applies if presses > 1. + The interval between multiple presses, only applies if presses > 1, defaul 0.1. """ + key = key.lower() + _check_key_exists(key) + for _ in range(presses): - try: - key_down(key) - key_up(key) - except KeyError: - raise exceptions.InvalidKeyRequested(key) + key_down(key) + key_up(key) if presses > 1: time.sleep(interval) @@ -172,13 +190,16 @@ def press(key: str, presses: int = 1, interval: int | float = 0.1) -> None: def write(term: str, interval: int | float = 0.05) -> None: """Writes a term by sending each key one after another. + Uppercase characters are not currently supported, the term will + come out as lowercase. + Parameters ---------- term :class:`str`: The term to write. interval :class:`int | float`: - The interval between pressing of the different characters. + The interval between the different characters, default 0.05. """ for c in term.lower(): press(c) @@ -206,16 +227,23 @@ def key_down(key: str, delay: Optional[float] = None) -> None: If you want to hold a key while performing an action, please use `hold_key`, which offers a context manager. """ - stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_DOWN, 0) - interception.send(keyboard, stroke) + key = key.lower() + _check_key_exists(key) + kcode = KEYBOARD_MAPPING[key] + stroke = KeyStroke(kcode, KeyState.KEY_DOWN, 0) + interception.send(keyboard, stroke) time.sleep(delay or KEY_PRESS_DELAY) @requires_driver def key_up(key: str, delay: Optional[float] = None) -> None: """Releases a key.""" - stroke = KeyStroke(KEYBOARD_MAPPING[key], KeyState.KEY_UP, 0) + key = key.lower() + _check_key_exists(key) + kcode = KEYBOARD_MAPPING[key] + stroke = KeyStroke(kcode, KeyState.KEY_UP, 0) + interception.send(keyboard, stroke) time.sleep(delay or KEY_PRESS_DELAY) @@ -227,9 +255,10 @@ def mouse_down(button: MouseButton, delay: Optional[float] = None) -> None: If you want to hold a mouse button while performing an action, please use `hold_mouse`, which offers a context manager. """ + _check_button_exists(button) down, _ = MouseState.from_string(button) - stroke = MouseStroke(down, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) + interception.send(mouse, stroke) time.sleep(delay or MOUSE_BUTTON_DELAY) @@ -237,9 +266,10 @@ def mouse_down(button: MouseButton, delay: Optional[float] = None) -> None: @requires_driver def mouse_up(button: MouseButton, delay: Optional[float] = None) -> None: """Releases a mouse button.""" + _check_button_exists(button) _, up = MouseState.from_string(button) - stroke = MouseStroke(up, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) + interception.send(mouse, stroke) time.sleep(delay or MOUSE_BUTTON_DELAY) @@ -382,3 +412,11 @@ def listen_to_mouse() -> int: context.send(device, stroke) finally: context._destroy_context() + +def _check_button_exists(button: MouseButton | str) -> None: + if button not in _SUPPORTED_BUTTONS: + raise exceptions.UnknownButtonError(button) + +def _check_key_exists(key: str) -> None: + if key not in _SUPPORTED_KEYS: + raise exceptions.UnknownKeyError(key) \ No newline at end of file diff --git a/src/interception/types.py b/src/interception/types.py index 51a4319..dae3a75 100644 --- a/src/interception/types.py +++ b/src/interception/types.py @@ -1,3 +1,3 @@ from typing import Literal -MouseButton = Literal["left", "right", "middle", "mouse4", "mouse5"] +MouseButton = str | Literal["left", "right", "middle", "mouse4", "mouse5"] From 334f5c946b4cbfdd1922b8c045af7ccf459d109e Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Sun, 13 Aug 2023 20:56:11 +0200 Subject: [PATCH 32/37] did some refactoring --- src/interception/_keycodes.py | 69 +++++++----- src/interception/exceptions.py | 10 +- src/interception/inputs.py | 181 ++++++++++++------------------- src/interception/interception.py | 4 +- 4 files changed, 119 insertions(+), 145 deletions(-) diff --git a/src/interception/_keycodes.py b/src/interception/_keycodes.py index edcda0f..33a4a0d 100644 --- a/src/interception/_keycodes.py +++ b/src/interception/_keycodes.py @@ -1,14 +1,38 @@ from ctypes import windll, wintypes KEYBOARD_MAPPING = { + "f1": 0x70, + "f2": 0x71, + "f3": 0x72, + "f4": 0x73, + "f5": 0x74, + "f6": 0x75, + "f7": 0x76, + "f8": 0x77, + "f9": 0x78, + "f10": 0x79, + "f11": 0x7A, + "f12": 0x7B, + "f13": 0x7C, + "f14": 0x7D, + "f15": 0x7E, + "f16": 0x7F, + "f17": 0x80, + "f18": 0x81, + "f19": 0x82, + "f20": 0x83, + "f21": 0x84, + "f22": 0x85, + "f23": 0x86, + "f24": 0x87, "backspace": 0x08, - "\b": 0x08, - "super": 0x5B, + "\b": 0x08, # same as backspace "tab": 0x09, - "\t": 0x09, + "\t": 0x09, # same as tab + "super": 0x5B, "clear": 0x0C, - "enter": 0x0D, - "\n": 0x0D, + "enter": 0x0D, + "\n": 0x0D, # same as enter key (newline) "return": 0x0D, "shift": 0x10, "ctrl": 0x11, @@ -67,35 +91,16 @@ "num8": 0x68, "num9": 0x69, "multiply": 0x6A, + "*": 0x6A, "add": 0x6B, + "plus": 0x6B, + "+": 0x6B, "separator": 0x6C, "subtract": 0x6D, + "minus": 0x6D, + "dash": 0x6D, "decimal": 0x6E, "divide": 0x6F, - "f1": 0x70, - "f2": 0x71, - "f3": 0x72, - "f4": 0x73, - "f5": 0x74, - "f6": 0x75, - "f7": 0x76, - "f8": 0x77, - "f9": 0x78, - "f10": 0x79, - "f11": 0x7A, - "f12": 0x7B, - "f13": 0x7C, - "f14": 0x7D, - "f15": 0x7E, - "f16": 0x7F, - "f17": 0x80, - "f18": 0x81, - "f19": 0x82, - "f20": 0x83, - "f21": 0x84, - "f22": 0x85, - "f23": 0x86, - "f24": 0x87, "numlock": 0x90, "scrolllock": 0x91, "shiftleft": 0xA0, @@ -129,4 +134,8 @@ KEYBOARD_MAPPING[chr(c).lower()] = windll.user32.VkKeyScanA(wintypes.WCHAR(chr(c))) for k, v in KEYBOARD_MAPPING.items(): - KEYBOARD_MAPPING[k] = windll.user32.MapVirtualKeyA(v, 0) \ No newline at end of file + KEYBOARD_MAPPING[k] = windll.user32.MapVirtualKeyA(v, 0) + + + + diff --git a/src/interception/exceptions.py b/src/interception/exceptions.py index f33e1b4..9fa75b5 100644 --- a/src/interception/exceptions.py +++ b/src/interception/exceptions.py @@ -15,7 +15,10 @@ def __init__(self, key: str) -> None: self.key = key def __str__(self) -> str: - return f"Unknown key requested: {self.key}" + return ( + f"Unknown key requested: {self.key}.\n" + "Consider running 'pyinterception show_supported_keys' for a list of all supported keys." + ) class UnknownButtonError(LookupError): @@ -25,4 +28,7 @@ def __init__(self, button: str) -> None: self.button = button def __str__(self) -> str: - return f"Unknown button requested: {self.button}" + return ( + f"Unknown button requested: {self.button}.\n" + "Consider running 'pyinterception show_supported_buttons' for a list of all supported buttons." + ) diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 02a8c02..fdf22b2 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -1,6 +1,8 @@ import functools import time from contextlib import contextmanager +from math import e +from re import L from typing import Literal, Optional from . import _utils, exceptions @@ -33,9 +35,6 @@ keyboard = 1 mouse = 11 -_SUPPORTED_BUTTONS = {"left", "right", "middle", "mouse4", "mouse5"} -_SUPPORTED_KEYS = dict(KEYBOARD_MAPPING) - def requires_driver(func): """Wraps any function that requires the interception driver to be installed @@ -132,7 +131,6 @@ def click( delay :class:`int | float`: The delay between moving and clicking, default 0.3. """ - _check_button_exists(button) if x is not None: move_to(x, y) time.sleep(delay) @@ -163,7 +161,7 @@ def right_click(clicks: int = 1, interval: int | float = 0.1) -> None: @requires_driver def press(key: str, presses: int = 1, interval: int | float = 0.1) -> None: - """Presses a key. + """Presses a given key, for mouse buttons use the`click` function. Parameters ---------- @@ -176,9 +174,6 @@ def press(key: str, presses: int = 1, interval: int | float = 0.1) -> None: interval :class:`int | float`: The interval between multiple presses, only applies if presses > 1, defaul 0.1. """ - key = key.lower() - _check_key_exists(key) - for _ in range(presses): key_down(key) key_up(key) @@ -209,40 +204,57 @@ def write(term: str, interval: int | float = 0.05) -> None: @requires_driver def scroll(direction: Literal["up", "down"]) -> None: """Scrolls the mouse wheel one unit in a given direction.""" - amount = ( - MouseRolling.MOUSE_WHEEL_UP - if direction == "up" - else MouseRolling.MOUSE_WHEEL_DOWN - ) + if direction == "up": + rolling = MouseRolling.MOUSE_WHEEL_UP + else: + rolling = MouseRolling.MOUSE_WHEEL_DOWN - stroke = MouseStroke(MouseState.MOUSE_WHEEL, 0, amount, 0, 0, 0) + stroke = MouseStroke(MouseState.MOUSE_WHEEL, 0, rolling, 0, 0, 0) interception.send(mouse, stroke) time.sleep(0.025) @requires_driver -def key_down(key: str, delay: Optional[float] = None) -> None: - """Holds a key down, will not be released automatically. +def key_down(key: str, delay: Optional[float | int] = None) -> None: + """Updates the state of the given key to be `down`. - If you want to hold a key while performing an action, please use - `hold_key`, which offers a context manager. + To release the key automatically, consider using the `hold_key` contextmanager. + + ### Parameters: + ---------- + key :class: `str`: + The key to hold down. + + delay :class: `Optional[float | int]`: + The amount of time to wait after updating the key state. + + ### Raises: + `UnknownKeyError` if the given key is not supported. """ - key = key.lower() - _check_key_exists(key) - kcode = KEYBOARD_MAPPING[key] - stroke = KeyStroke(kcode, KeyState.KEY_DOWN, 0) + keycode = _get_keycode(key) + stroke = KeyStroke(keycode, KeyState.KEY_DOWN, 0) interception.send(keyboard, stroke) time.sleep(delay or KEY_PRESS_DELAY) @requires_driver -def key_up(key: str, delay: Optional[float] = None) -> None: - """Releases a key.""" - key = key.lower() - _check_key_exists(key) - kcode = KEYBOARD_MAPPING[key] - stroke = KeyStroke(kcode, KeyState.KEY_UP, 0) +def key_up(key: str, delay: Optional[float | int] = None) -> None: + """Updates the state of the given key to be `up`. + + ### Parameters: + ---------- + key :class: `str`: + The key to release. + + delay :class: `Optional[float | int]`: + The amount of time to wait after updating the key state. + + ### Raises: + `UnknownKeyError` if the given key is not supported. + """ + keycode = _get_keycode(key) + stroke = KeyStroke(keycode, KeyState.KEY_UP, 0) interception.send(keyboard, stroke) time.sleep(delay or KEY_PRESS_DELAY) @@ -255,10 +267,8 @@ def mouse_down(button: MouseButton, delay: Optional[float] = None) -> None: If you want to hold a mouse button while performing an action, please use `hold_mouse`, which offers a context manager. """ - _check_button_exists(button) - down, _ = MouseState.from_string(button) - stroke = MouseStroke(down, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) - + button_state = _get_button_states(button, down=True) + stroke = MouseStroke(button_state, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) interception.send(mouse, stroke) time.sleep(delay or MOUSE_BUTTON_DELAY) @@ -266,10 +276,8 @@ def mouse_down(button: MouseButton, delay: Optional[float] = None) -> None: @requires_driver def mouse_up(button: MouseButton, delay: Optional[float] = None) -> None: """Releases a mouse button.""" - _check_button_exists(button) - _, up = MouseState.from_string(button) - stroke = MouseStroke(up, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) - + button_state = _get_button_states(button, down=False) + stroke = MouseStroke(button_state, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) interception.send(mouse, stroke) time.sleep(delay or MOUSE_BUTTON_DELAY) @@ -309,32 +317,21 @@ def hold_key(key: str): @requires_driver -def capture_keyboard() -> int: +def capture_keyboard() -> None: """Captures keyboard keypresses until the `Escape` key is pressed. Filters out non `KEY_DOWN` events to not post the same capture twice. """ context = Interception() context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) - print("Capturing keyboard presses, press ESC to quit.") - try: - while True: - device = context.wait() - stroke = context.receive(device) - - if stroke.code == 0x01: - print("ESC pressed, exited.") - return device - print(f"Received stroke {stroke} on keyboard device {device}") - context.send(device, stroke) - finally: - context._destroy_context() + _listen_to_events(context, "esc") + print("No longer intercepting mouse events.") @requires_driver -def capture_mouse() -> int: +def capture_mouse() -> None: """Captures mouse left clicks until the `Escape` key is pressed. Filters out non `LEFT_BUTTON_DOWN` events to not post the same capture twice. @@ -342,81 +339,43 @@ def capture_mouse() -> int: context = Interception() context.set_filter(context.is_mouse, FilterMouseState.FILTER_MOUSE_LEFT_BUTTON_DOWN) context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) + print("Intercepting mouse left clicks, press ESC to quit.") - print("Capturing mouse left clicks, press ESC to quit.") - try: - while True: - device = context.wait() - stroke = context.receive(device) - - if context.is_keyboard(device) and stroke.code == 0x01: - print("ESC pressed, exited.") - return device - - elif not context.is_keyboard(device): - print(f"Received stroke {stroke} on mouse device {device}") + _listen_to_events(context, "esc") + print("No longer intercepting mouse events.") - context.send(device, stroke) - finally: - context._destroy_context() +def _listen_to_events(context: Interception, stop_button: str) -> None: + """Listens to a given interception context. Stops when the `stop_button` is + the event key. -@requires_driver -def listen_to_keyboard() -> int: - """Captures keyboard keypresses until the `Escape` key is pressed. - - Doesn't filter out any events at all. - """ - context = Interception() - context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_ALL) - - print("Listenting to keyboard, press ESC to quit.") + Remember to destroy the context in any case afterwards. Otherwise events + will continue to be intercepted!""" + stop = _get_keycode(stop_button) try: while True: device = context.wait() stroke = context.receive(device) - if stroke.code == 0x01: - print("ESC pressed, exited.") - return device + if context.is_keyboard(device) and stroke.code == stop: + return - print(f"Received stroke {stroke} on keyboard device {device}") + print(f"Received stroke {stroke} on mouse device {device}") context.send(device, stroke) finally: - context._destroy_context() + context.destroy() -@requires_driver -def listen_to_mouse() -> int: - """Captures mouse movements / clicks until the `Escape` key is pressed. - - Doesn't filter out any events at all. - """ - context = Interception() - context.set_filter(context.is_mouse, FilterMouseState.FILTER_MOUSE_ALL) - context.set_filter(context.is_keyboard, FilterKeyState.FILTER_KEY_DOWN) - - print("Listenting to mouse, press ESC to quit.") +def _get_keycode(key: str) -> int: try: - while True: - device = context.wait() - stroke = context.receive(device) + return KEYBOARD_MAPPING[key] + except KeyError: + raise exceptions.UnknownKeyError(key) - if context.is_keyboard(device) and stroke.code == 0x01: - print("ESC pressed, exited.") - return device - elif not context.is_keyboard(device): - print(f"Received stroke {stroke} on mouse device {device}") - - context.send(device, stroke) - finally: - context._destroy_context() - -def _check_button_exists(button: MouseButton | str) -> None: - if button not in _SUPPORTED_BUTTONS: +def _get_button_states(button: str, *, down: bool) -> int: + try: + states = MouseState.from_string(button) + return states[not down] # first state is down, second state is up + except KeyError: raise exceptions.UnknownButtonError(button) - -def _check_key_exists(key: str) -> None: - if key not in _SUPPORTED_KEYS: - raise exceptions.UnknownKeyError(key) \ No newline at end of file diff --git a/src/interception/interception.py b/src/interception/interception.py index 0825b38..fed3ab8 100644 --- a/src/interception/interception.py +++ b/src/interception/interception.py @@ -19,7 +19,7 @@ def __init__(self) -> None: try: self.build_handles() except Exception as e: - self._destroy_context() + self.destroy() raise e def build_handles(self) -> None: @@ -129,6 +129,6 @@ def create_device_handle(device_num: int): device_name = f"\\\\.\\interception{device_num:02d}".encode() return k32.CreateFileA(device_name, 0x80000000, 0, 0, 3, 0, 0) - def _destroy_context(self): + def destroy(self) -> None: for device in self._context: device.destroy() From cfdd6e019356b980a2f2d4c2fefc678ebb098ac6 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Sun, 13 Aug 2023 20:57:33 +0200 Subject: [PATCH 33/37] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2c0f4d5..1df28aa 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ Now, once you have all of that set up, you can go ahead and import `interception ```py import interception -kdevice = interception.listen_to_keyboard() -mdevice = interception.listen_to_mouse() +kdevice = interception.capture_keyboard() +mdevice = interception.capture_mouse() ``` You will get two integers back, those integers are the number of the device you just used. Let's set this device in interception to ensure it sends events from the correct one! ```py From 09f13ad9b7d4dc318678b877791394f0e1ee2506 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Sun, 13 Aug 2023 20:58:30 +0200 Subject: [PATCH 34/37] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1df28aa..745b2cc 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,10 @@ Now, once you have all of that set up, you can go ahead and import `interception ```py import interception -kdevice = interception.capture_keyboard() -mdevice = interception.capture_mouse() +interception.capture_keyboard() +interception.capture_mouse() ``` -You will get two integers back, those integers are the number of the device you just used. Let's set this device in interception to ensure it sends events from the correct one! +You will get two integers back in the terminal, those integers are the number of the device you just used. Let's set this device in interception to ensure it sends events from the correct one! ```py interception.inputs.keyboard = kdevice interception.inputs.mouse = mdevice From e3a5acc678f6750b5f5dfec30354012a7af25e3f Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Tue, 15 Aug 2023 20:41:34 +0200 Subject: [PATCH 35/37] auto capture & improvements --- pyproject.toml | 8 +-- setup.cfg | 5 +- src/interception/_consts.py | 1 - src/interception/_utils.py | 20 +++++- src/interception/inputs.py | 109 +++++++++++++++++++++++-------- src/interception/interception.py | 39 +++++++++-- 6 files changed, 143 insertions(+), 39 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4af37a0..02d1b81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,12 +3,12 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "pyintercept" +name = "pyinterception" version = "0.0.0" authors = [ { name="Kenny Hommel", email="kennyhommel36@gmail.com" }, ] -description = "Pyintercept" +description = "pyinterception" readme = "README.md" requires-python = ">=3.10" classifiers = [ @@ -18,5 +18,5 @@ classifiers = [ ] [project.urls] -"Homepage" = "https://github.com/kennyhml/pyintercept" -"Bug Tracker" = "https://github.com/kennyhml/pyintercept/issues" \ No newline at end of file +"Homepage" = "https://github.com/kennyhml/pyinterception" +"Bug Tracker" = "https://github.com/kennyhml/pyinterception/issues" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index ed0cdef..2e29326 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -name = "pyintercept" +name = "pyinterception" license_files = LICENSE [options] @@ -8,7 +8,8 @@ package_dir= packages = find: zip_safe = False python_requires = >= 3 - +install_requires = + pynput [options.packages.find] where = src diff --git a/src/interception/_consts.py b/src/interception/_consts.py index 71f0f92..5bb6715 100644 --- a/src/interception/_consts.py +++ b/src/interception/_consts.py @@ -1,6 +1,5 @@ from __future__ import annotations from enum import IntEnum -from typing import Literal class KeyState(IntEnum): diff --git a/src/interception/_utils.py b/src/interception/_utils.py index ebba8b9..88d6fb4 100644 --- a/src/interception/_utils.py +++ b/src/interception/_utils.py @@ -1,5 +1,6 @@ from typing import Optional - +import functools +from threading import Thread import win32api # type: ignore @@ -53,3 +54,20 @@ def scale(dimension: int, point: int) -> int: def get_cursor_pos() -> tuple[int, int]: """Gets the current position of the cursor using `GetCursorPos`""" return win32api.GetCursorPos() + + +def threaded(name: str): + """Threads a function, beware that it will lose its return values""" + + def outer(func): + @functools.wraps(func) + def inner(*args, **kwargs): + def run(): + func(*args, **kwargs) + + thread = Thread(target=run, name=name) + thread.start() + + return inner + + return outer diff --git a/src/interception/inputs.py b/src/interception/inputs.py index fdf22b2..5175f3b 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -1,22 +1,18 @@ import functools +import random import time from contextlib import contextmanager -from math import e -from re import L from typing import Literal, Optional +from pynput.keyboard import Listener as KeyListener # type: ignore[import] +from pynput.mouse import Listener as MouseListener # type: ignore[import] + from . import _utils, exceptions -from ._consts import ( - FilterKeyState, - FilterMouseState, - KeyState, - MouseFlag, - MouseRolling, - MouseState, -) +from ._consts import (FilterKeyState, FilterMouseState, KeyState, MouseFlag, + MouseRolling, MouseState) from ._keycodes import KEYBOARD_MAPPING from .interception import Interception -from .strokes import KeyStroke, MouseStroke +from .strokes import KeyStroke, MouseStroke, Stroke from .types import MouseButton # try to initialize interception, if it fails simply remember that it failed to initalize. @@ -32,8 +28,9 @@ MOUSE_BUTTON_DELAY = 0.03 KEY_PRESS_DELAY = 0.025 -keyboard = 1 -mouse = 11 + +_TEST_MOUSE_STROKE = MouseStroke(MouseState.MOUSE_MIDDLE_BUTTON_UP, 0, 0, 0, 0, 0) +_TEST_KEY_STROKE = KeyStroke(KEYBOARD_MAPPING["space"], KeyState.KEY_UP, 0) def requires_driver(func): @@ -74,7 +71,7 @@ def move_to(x: int | tuple[int, int], y: Optional[int] = None) -> None: x, y = _utils.to_interception_coordinate(x, y) stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, x, y, 0) - interception.send(mouse, stroke) + interception.send_mouse(stroke) @requires_driver @@ -95,7 +92,7 @@ def move_relative(x: int = 0, y: int = 0) -> None: >>> 400, 400 """ stroke = MouseStroke(0, MouseFlag.MOUSE_MOVE_RELATIVE, 0, x, y, 0) - interception.send(mouse, stroke) + interception.send_mouse(stroke) def mouse_position() -> tuple[int, int]: @@ -210,7 +207,7 @@ def scroll(direction: Literal["up", "down"]) -> None: rolling = MouseRolling.MOUSE_WHEEL_DOWN stroke = MouseStroke(MouseState.MOUSE_WHEEL, 0, rolling, 0, 0, 0) - interception.send(mouse, stroke) + interception.send_mouse(stroke) time.sleep(0.025) @@ -233,8 +230,7 @@ def key_down(key: str, delay: Optional[float | int] = None) -> None: """ keycode = _get_keycode(key) stroke = KeyStroke(keycode, KeyState.KEY_DOWN, 0) - - interception.send(keyboard, stroke) + interception.send_key(stroke) time.sleep(delay or KEY_PRESS_DELAY) @@ -255,8 +251,7 @@ def key_up(key: str, delay: Optional[float | int] = None) -> None: """ keycode = _get_keycode(key) stroke = KeyStroke(keycode, KeyState.KEY_UP, 0) - - interception.send(keyboard, stroke) + interception.send_key(stroke) time.sleep(delay or KEY_PRESS_DELAY) @@ -269,7 +264,7 @@ def mouse_down(button: MouseButton, delay: Optional[float] = None) -> None: """ button_state = _get_button_states(button, down=True) stroke = MouseStroke(button_state, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) - interception.send(mouse, stroke) + interception.send_mouse(stroke) time.sleep(delay or MOUSE_BUTTON_DELAY) @@ -278,16 +273,16 @@ def mouse_up(button: MouseButton, delay: Optional[float] = None) -> None: """Releases a mouse button.""" button_state = _get_button_states(button, down=False) stroke = MouseStroke(button_state, MouseFlag.MOUSE_MOVE_ABSOLUTE, 0, 0, 0, 0) - interception.send(mouse, stroke) + interception.send_mouse(stroke) time.sleep(delay or MOUSE_BUTTON_DELAY) @requires_driver @contextmanager def hold_mouse(button: MouseButton): - """A context manager to hold a mouse button while performing another action. + """Holds a mouse button down while performing another action. - Example: + ### Example: ```py with interception.hold_mouse("left"): interception.move_to(300, 300) @@ -302,9 +297,9 @@ def hold_mouse(button: MouseButton): @requires_driver @contextmanager def hold_key(key: str): - """A context manager to hold a mouse button while performing another action. + """Hold a key down while performing another action. - Example: + ### Example: ```py with interception.hold_key("ctrl"): interception.press("c") @@ -345,6 +340,68 @@ def capture_mouse() -> None: print("No longer intercepting mouse events.") +@requires_driver +def auto_capture_devices( + *, keyboard: bool = True, mouse: bool = True, verbose: bool = False +) -> None: + """Uses pynputs keyboard and mouse listener to check whether a device + number will send a valid input. During this process, each possible number + for the device is tried - once a working number is found, it is assigned + to the context and the it moves to the next device. + + ### Parameters: + -------------- + keyboard :class:`bool`: + Capture the keyboard number. + + mouse :class:`bool`: + Capture the mouse number. + + verbose :class:`bool`: + Provide output regarding the tested numbers. + """ + + def log(info: str) -> None: + if verbose: + print(info) + + mouse_listener = MouseListener(on_click=lambda *args: False) + key_listener = KeyListener(on_release=lambda *args: False) + + for device in ("keyboard", "mouse"): + if (device == "keyboard" and not keyboard) or (device == "mouse" and not mouse): + continue + log(f"Trying {device} device numbers...") + stroke: Stroke + if device == "mouse": + listener, stroke, nums = mouse_listener, _TEST_MOUSE_STROKE, range(10, 20) + else: + listener, stroke, nums = key_listener, _TEST_KEY_STROKE, range(10) + + listener.start() + for num in nums: + interception.send(num, stroke) + time.sleep(random.uniform(0.1, 0.3)) + if listener.is_alive(): + log(f"No success on {device} {num}...") + continue + log(f"Success on {device} {num}!") + set_devices(**{device: num}) + break + + log("Devices set.") + + +def set_devices(keyboard: Optional[int] = None, mouse: Optional[int] = None) -> None: + """Sets the devices on the current context. Keyboard devices should be from 0 to 10 + and mouse devices from 10 to 20 (both non-inclusive). + + If a device out of range is passed, the context will raise a `ValueError`. + """ + interception.keyboard = keyboard or interception.keyboard + interception.mouse = mouse or interception.mouse + + def _listen_to_events(context: Interception, stop_button: str) -> None: """Listens to a given interception context. Stops when the `stop_button` is the event key. diff --git a/src/interception/interception.py b/src/interception/interception.py index fed3ab8..c961b76 100644 --- a/src/interception/interception.py +++ b/src/interception/interception.py @@ -15,6 +15,8 @@ class Interception: def __init__(self) -> None: self._context: list[Device] = [] self._c_events: Array[c_void_p] = (c_void_p * MAX_DEVICES)() + self._mouse = 11 + self._keyboard = 1 try: self.build_handles() @@ -22,6 +24,28 @@ def __init__(self) -> None: self.destroy() raise e + @property + def mouse(self) -> int: + return self._mouse + + @mouse.setter + def mouse(self, num: int) -> None: + if self.is_invalid(num) or not self.is_mouse(num): + raise ValueError(f"{num} is not a valid mouse number (10 <= mouse <= 19).") + self._mouse = num + + @property + def keyboard(self) -> int: + return self._keyboard + + @keyboard.setter + def keyboard(self, num: int) -> None: + if self.is_invalid(num) or not self.is_keyboard(num): + raise ValueError( + f"{num} is not a valid keyboard number (0 <= keyboard <= 9)." + ) + self._keyboard = num + def build_handles(self) -> None: """Creates handles and events for all interception devices. @@ -90,11 +114,16 @@ def receive(self, device: int): if not self.is_invalid(device): return self._context[device].receive() - def send(self, device: int, stroke: Stroke): - if not self.is_invalid(device): - self._context[device].send(stroke) - - def set_filter(self,predicate,filter): + def send(self, device: int, stroke: Stroke) -> None: + self._context[device].send(stroke) + + def send_key(self, stroke: Stroke) -> None: + self._context[self._keyboard].send(stroke) + + def send_mouse(self, stroke: Stroke) -> None: + self._context[self._mouse].send(stroke) + + def set_filter(self, predicate, filter): for i in range(MAX_DEVICES): if predicate(i): result = self._context[i].set_filter(filter) From b278ef276b5b725869b66cb2b13d604fd002aac9 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Tue, 15 Aug 2023 21:13:09 +0200 Subject: [PATCH 36/37] Update README.md --- README.md | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 745b2cc..f125078 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ This is a greatly reworked version of [interception_py][wrp], a python port for The Interception API aims to build a portable programming interface that allows one to intercept and control a range of input devices. +## How to install +Pyinterception is now available on PyPi under the name `interception-python`! So simply `pip install interception-python`. + + ## Why use interception? Did you ever try to send inputs to an application or game and, well, nothing happened? Sure, in alot of cases this is resolved by running your code with administrative privileges, but this is not always the case. @@ -38,32 +42,33 @@ Why is this bad? Well, it's not always bad. If whatever you're sending inputs to Alright, enough about that, onto the important shit. ## Why use this fork? -- I aim to make this port as simple to use as possible -- Comparable to things like pyautogui or pydirectinput -- The codebase has been refactored in a much more readable fashion +- Extremely simple interface, comparable to pyautogui / pydirectinput +- Dynamic keyboard adjustment for all kinds of layouts +- Refactored in a much more readable and documented fashion - I work with loads of automation first hand so there is alot of QoL features. -Unfortunately, the original repository is entirely undocumented and stuffed into one file for the most part, -which made some things pretty hard to decipher and understand. - - ## How to use? First of all, you absolutely need to install the [interception-driver][c_ception], otherwise none of this will work. It's a very simple install. -Now, once you have all of that set up, you can go ahead and import `interception`. Let's start by identifying your used devices! - +Now, once you have all of that set up, you can go ahead and import `interception`. +The first thing you need to understand is that you have 10 different numbers for keyboard / mouse, and any of them could be the device you are +using. You can observe this by running the following program: ```py import interception interception.capture_keyboard() interception.capture_mouse() ``` -You will get two integers back in the terminal, those integers are the number of the device you just used. Let's set this device in interception to ensure it sends events from the correct one! +You can cancel the capture by pressing the ESC key, but every time you press a key or click with the mouse, you can see the intercepted event in the terminal. +The event consists of different kinds of flags and states, but also of the number of your device we just talked about. + +To make sure that interception can actively send inputs from the correct devices, you have to set the correct devices. You can do this by manually checking the output, +but that gets pretty annoying as they can and will change sometimes. To make this easier, pyinterception has a method that will automatically capture a working device: ```py -interception.inputs.keyboard = kdevice -interception.inputs.mouse = mdevice -``` +import interception +interception.auto_capture_devices(keyboard=True, mouse=True) +``` So, now you can begin to send inputs, just like you are used to it from pyautogui or pydirectinput! ```py interception.move_to(960, 540) From ab2fafbaa4f6b615d6fb19692662ee0cc9c58197 Mon Sep 17 00:00:00 2001 From: Kenny <106347478+kennyhml@users.noreply.github.com> Date: Thu, 17 Aug 2023 03:08:28 +0200 Subject: [PATCH 37/37] added requires_driver to set_devices --- pyproject.toml | 4 ++-- setup.cfg | 2 +- src/interception/inputs.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 02d1b81..06f3e34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,12 +3,12 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "pyinterception" +name = "interception-python" version = "0.0.0" authors = [ { name="Kenny Hommel", email="kennyhommel36@gmail.com" }, ] -description = "pyinterception" +description = "A python port of interception, which hooks into the input event handling mechanisms to simulate inputs without injected flags" readme = "README.md" requires-python = ">=3.10" classifiers = [ diff --git a/setup.cfg b/setup.cfg index 2e29326..c19d646 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -name = "pyinterception" +name = "interception-python" license_files = LICENSE [options] diff --git a/src/interception/inputs.py b/src/interception/inputs.py index 5175f3b..7cfe1a9 100644 --- a/src/interception/inputs.py +++ b/src/interception/inputs.py @@ -391,7 +391,7 @@ def log(info: str) -> None: log("Devices set.") - +@requires_driver def set_devices(keyboard: Optional[int] = None, mouse: Optional[int] = None) -> None: """Sets the devices on the current context. Keyboard devices should be from 0 to 10 and mouse devices from 10 to 20 (both non-inclusive).