reywechat-hook 1.0.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,7 @@
1
+ Copyright 2025 ReyXBo
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: reywechat-hook
3
+ Version: 1.0.10
4
+ Summary: WeChat client control hook method set.
5
+ Project-URL: Homepage, https://www.reyxbo.com
6
+ Project-URL: Repository, https://github.com/reyxbo/reywechat-hook-py.git
7
+ Author-email: Rey <reyxbo@163.com>
8
+ License: Copyright 2025 ReyXBo
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15
+ License-File: LICENSE
16
+ Keywords: helper,hook,rey,reyxbo,robot,wechat
17
+ Requires-Python: >=3.12
18
+ Description-Content-Type: text/markdown
19
+
20
+ # reywechat
21
+
22
+ > WeChat client control hook method set.
23
+
24
+ ## Install
25
+
26
+ ```
27
+ pip install reywechat
28
+ ```
@@ -0,0 +1,9 @@
1
+ # reywechat
2
+
3
+ > WeChat client control hook method set.
4
+
5
+ ## Install
6
+
7
+ ```
8
+ pip install reywechat
9
+ ```
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "reywechat-hook"
3
+ version = "1.0.10"
4
+ description = "WeChat client control hook method set."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = {file = "LICENSE"}
8
+ keywords = ["rey", "reyxbo", "wechat", "robot", "helper", "hook"]
9
+ authors = [
10
+ {name = "Rey", email = "reyxbo@163.com"}
11
+ ]
12
+ dependencies = [
13
+ ]
14
+
15
+ [project.urls]
16
+ Homepage = "https://www.reyxbo.com"
17
+ Repository = "https://github.com/reyxbo/reywechat-hook-py.git"
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ @Time : 2023-10-17
5
+ @Author : Rey
6
+ @Contact : reyxbo@163.com
7
+ @Explain : WeChat client control hook method set.
8
+
9
+ Folders
10
+ -------
11
+ data : Data file.
12
+
13
+ Modules
14
+ -------
15
+ rall : All import methods.
16
+ rbase : Base methods.
17
+ rhook : Hook methods.
18
+ """
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ @Time : 2023-10-19
5
+ @Author : Rey
6
+ @Contact : reyxbo@163.com
7
+ @Explain : All import methods.
8
+ """
9
+
10
+ # ruff: noqa: F403
11
+
12
+ from .rbase import *
13
+ from .rhook import *
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ @Time : 2026-07-02
5
+ @Author : Rey
6
+ @Contact : reyxbo@163.com
7
+ @Explain : Base methods.
8
+ """
9
+
10
+ from typing import Any, Literal, Callable, overload
11
+ from sys import maxsize as sys_maxsize
12
+ from traceback import print_exc
13
+ from socket import socket as Socket
14
+ from json import JSONDecodeError, loads as json_loads, dumps as json_dumps
15
+
16
+ __all__ = (
17
+ 'get_sys_bits',
18
+ 'listen_socket',
19
+ 'send_socket'
20
+ )
21
+
22
+ def get_sys_bits() -> Literal[32, 64]:
23
+ """
24
+ Get number of bits in the system.
25
+
26
+ Returns
27
+ -------
28
+ Number of bits in the system.
29
+ """
30
+
31
+ # Judge.
32
+ if sys_maxsize == 2147483647:
33
+ return 32
34
+ elif sys_maxsize == 9223372036854775807:
35
+ return 64
36
+
37
+ @overload
38
+ def listen_socket(
39
+ host: str,
40
+ port: str | int,
41
+ handler: Callable[[Any], None]
42
+ ) -> None: ...
43
+
44
+ @overload
45
+ def listen_socket(
46
+ host: str,
47
+ port: str | int,
48
+ handler: Callable[[bytes], None],
49
+ auto_decode: Literal[False]
50
+ ) -> None: ...
51
+
52
+ def listen_socket(
53
+ host: str,
54
+ port: str | int,
55
+ handler: Callable[[Any], None],
56
+ auto_decode: bool = True
57
+ ) -> None:
58
+ """
59
+ Listen socket and handle data.
60
+
61
+ Parameters
62
+ ----------
63
+ host : Socket host.
64
+ port : Socket port.
65
+ handler : Handler function.
66
+ auto_decode : Is it automatically decode to JSON.
67
+ """
68
+
69
+ # Parameter.
70
+ port = int(port)
71
+ rece_size = 1073741824 # 1024 * 1024 * 1024
72
+
73
+ # Listen.
74
+ with Socket() as socket:
75
+ socket.bind((host, port))
76
+ socket.listen()
77
+ socket.settimeout(1)
78
+
79
+ ## Loop.
80
+ while True:
81
+ try:
82
+ socket_conn, _ = socket.accept()
83
+ except TimeoutError:
84
+ continue
85
+ with socket_conn:
86
+ socket_conn.settimeout(1)
87
+ while True:
88
+ try:
89
+ data = socket_conn.recv(rece_size)
90
+ except TimeoutError:
91
+ continue
92
+ if not data:
93
+ break
94
+
95
+ ### Decode.
96
+ if auto_decode:
97
+ try:
98
+ data = json_loads(data.decode('utf-8'))
99
+ except JSONDecodeError:
100
+ print_exc()
101
+ continue
102
+
103
+ handler(data)
104
+
105
+ def send_socket(
106
+ host: str,
107
+ port: str | int,
108
+ data: Any,
109
+ ) -> None:
110
+ """
111
+ Send socket data.
112
+
113
+ Parameters
114
+ ----------
115
+ host : Socket host.
116
+ port : Socket port.
117
+ data : Socket data.
118
+ """
119
+
120
+ # Parameter.
121
+ port = int(port)
122
+ if not isinstance(data, bytes):
123
+ data: bytes = json_dumps(data, ensure_ascii=False).encode('utf-8')
124
+
125
+ # Send.
126
+ with Socket() as socket:
127
+ socket.connect((host, port))
128
+ socket.send(data)
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ @Time : 2026-07-02
5
+ @Author : Rey
6
+ @Contact : reyxbo@163.com
7
+ @Explain : Hook methods.
8
+ """
9
+
10
+ from typing import Any, Dict, TypedDict, Literal, Callable, Final
11
+ from os.path import exists as os_exists, abspath as os_abspath, split as os_split, join as os_join
12
+ from json import dumps as json_dumps, loads as json_loads
13
+ import ctypes
14
+
15
+ from .rbase import get_sys_bits, listen_socket, send_socket
16
+
17
+ __all__ = (
18
+ 'WeChatHookLoader',
19
+ 'WeChatHook'
20
+ )
21
+
22
+ LoginInfo = TypedDict(
23
+ 'LoginInfo',
24
+ {
25
+ 'id': str,
26
+ 'account': str,
27
+ 'name': str,
28
+ 'phone': str | None,
29
+ 'head_image': str | None,
30
+ 'file_dir': str
31
+ }
32
+ )
33
+ """
34
+ Key "id" is account ID.
35
+ Key "account" is account name.
36
+ Key "name" is nickname.
37
+ Key "phone" is phone number.
38
+ Key "head_image" is head image URL.
39
+ """
40
+
41
+ _path_dir: Final[str] = os_split(os_abspath(__file__))[0]
42
+ _path_helper_dll: Final[str] = os_join(_path_dir, 'data', 'Helper_4.1.2.17.dll')
43
+ _path_loader_dll: Final[str] = os_join(_path_dir, 'data', 'Loader_4.1.2.17.dll')
44
+ _socket_id: int | None = None
45
+ _recv_callback: Callable[[int, int, dict], None] | None = None
46
+ def _c_str(text: str | bytes) -> ctypes.c_char_p:
47
+ return ctypes.c_char_p(text.encode('utf-8'))
48
+
49
+ @ctypes.WINFUNCTYPE(None, ctypes.c_void_p)
50
+ def _on_connect(client_id: int) -> None:
51
+ """
52
+ Called when a new WeChat socket client is connected.
53
+
54
+ Parameters
55
+ ----------
56
+ client_id : Socket client ID.
57
+ """
58
+
59
+ # Handle.
60
+ global _socket_id
61
+ _socket_id = int(client_id)
62
+
63
+ @ctypes.WINFUNCTYPE(None, ctypes.c_long, ctypes.c_char_p, ctypes.c_ulong)
64
+ def _on_recv(client_id: int, data: bytes, length: int) -> None:
65
+ """
66
+ Called when WeChat sends a message.
67
+
68
+ Parameters
69
+ ----------
70
+ client_id : Socket client ID.
71
+ data : Raw JSON data pointer.
72
+ length : Data length.
73
+ """
74
+
75
+ # Handle.
76
+ global _recv_callback
77
+ if not data or not _recv_callback:
78
+ return
79
+ try:
80
+ raw = ctypes.string_at(data, length)
81
+ text = raw.decode('utf-8', errors='ignore').rstrip('\x00')
82
+ if not text:
83
+ return
84
+ try:
85
+ packet = json_loads(text)
86
+ except Exception:
87
+ end = text.rfind('}')
88
+ if end == -1:
89
+ return
90
+ packet = json_loads(text[:end + 1])
91
+ _recv_callback(
92
+ int(client_id),
93
+ packet.get('type'),
94
+ packet.get('data'),
95
+ )
96
+ except Exception:
97
+ pass
98
+
99
+ @ctypes.WINFUNCTYPE(None, ctypes.c_ulong)
100
+ def _on_close(client_id: int) -> None:
101
+ """
102
+ Called when a socket client disconnects.
103
+
104
+ Parameters
105
+ ----------
106
+ client_id : Socket client ID.
107
+ """
108
+
109
+ # Handle.
110
+ global _socket_id
111
+ if _socket_id == int(client_id):
112
+ _socket_id = None
113
+
114
+ class WeChatHookLoader(object):
115
+ """
116
+ WeChat client hook loader.
117
+ """
118
+
119
+ OFFSET_SOCKET = 0xB080
120
+ OFFSET_SEND = 0xAF90
121
+ OFFSET_INJECT = 0xCC10
122
+ OFFSET_DESTROY = 0xC540
123
+ OFFSET_UTF8 = 0xC680
124
+
125
+ def __init__(self) -> None:
126
+ """
127
+ Build instance attributes.
128
+ """
129
+
130
+ # Build.
131
+ if not os_exists(_path_loader_dll):
132
+ raise FileNotFoundError(_path_loader_dll)
133
+ dll = ctypes.WinDLL(_path_loader_dll)
134
+ self.base = dll._handle
135
+ self._init_shared_memory()
136
+ self._enable_utf8()
137
+ self._init_socket()
138
+
139
+ def _call(self, offset: int, argtypes: list, restype) -> ctypes._CFuncPtr:
140
+ """
141
+ Call function by offset.
142
+
143
+ Parameters
144
+ ----------
145
+ offset : Function offset relative to the Loader.dll base address.
146
+ argtypes : List of ctypes parameter types.
147
+ restype : ctypes return type.
148
+
149
+ Returns
150
+ -------
151
+ Callable ctypes function object.
152
+ """
153
+
154
+ # Function.
155
+ func = ctypes.WINFUNCTYPE(restype, *argtypes)
156
+ result = func(self.base + offset)
157
+
158
+ return result
159
+
160
+ @staticmethod
161
+ def _init_shared_memory() -> None:
162
+ """
163
+ Initialize create shared memory required by Loader/Helper.
164
+ """
165
+
166
+ # Initialize.
167
+ k32 = ctypes.windll.kernel32
168
+ mapping = k32.CreateFileMappingA(
169
+ -1, None, 0x04, 0, 33, b'windows_shell_global__'
170
+ )
171
+ if not mapping:
172
+ return
173
+ addr = k32.MapViewOfFile(mapping, 0x000F001F, 0, 0, 0)
174
+ if not addr:
175
+ return
176
+ key = b'3101b223dca7715b0154924f0eeeee20'
177
+ ctypes.memmove(addr, ctypes.create_string_buffer(key), len(key))
178
+
179
+ def _enable_utf8(self) -> None:
180
+ """
181
+ Enable UTF-8 communication mode.
182
+ """
183
+
184
+ # Enable.
185
+ self._call(self.OFFSET_UTF8, [], ctypes.c_bool)()
186
+
187
+ def _init_socket(self) -> None:
188
+ """
189
+ Initialize socket callbacks.
190
+ """
191
+
192
+ # Initialize.
193
+ self._call(
194
+ self.OFFSET_SOCKET,
195
+ [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p],
196
+ ctypes.c_bool,
197
+ )(_on_connect, _on_recv, _on_close)
198
+
199
+ def inject(self) -> int:
200
+ """
201
+ Inject `DLL` file into WeChat.
202
+
203
+ Returns
204
+ -------
205
+ Process ID of the injected WeChat process.
206
+ """
207
+
208
+ # Inject.
209
+ process_id = self._call(
210
+ self.OFFSET_INJECT,
211
+ [ctypes.c_char_p],
212
+ ctypes.c_uint32,
213
+ )(_c_str(_path_helper_dll))
214
+
215
+ return process_id
216
+
217
+ def send(self, client_id: int, text: str) -> bool:
218
+ """
219
+ Send raw JSON message to WeChat.
220
+
221
+ Args:
222
+ client_id (int): socket client id
223
+ text (str): JSON string
224
+
225
+ Returns:
226
+ bool: success
227
+ """
228
+ return self._call(
229
+ self.OFFSET_SEND,
230
+ [ctypes.c_uint32, ctypes.c_char_p],
231
+ ctypes.c_bool,
232
+ )(client_id, _c_str(text))
233
+
234
+ def destroy(self) -> bool:
235
+ """
236
+ Release Loader resources.
237
+
238
+ Returns:
239
+ bool
240
+ """
241
+ return self._call(self.OFFSET_DESTROY, [], ctypes.c_bool)()
242
+
243
+ class WeChatHook(object):
244
+ """
245
+ WeChat hook.
246
+ """
247
+
248
+ def __init__(
249
+ self,
250
+ send_port: int | str = 49152,
251
+ receive_port: int | str = 49153,
252
+ echo: bool = True
253
+ ):
254
+ """
255
+ Build instance attributes.
256
+
257
+ Parameters
258
+ ----------
259
+ self.send_port : Hook program listening port send message.
260
+ self.receive_port : Main program listening port receive message.
261
+ echo : Whether report callback message.
262
+ """
263
+
264
+ # Check System bits.
265
+ sys_bits = get_sys_bits()
266
+ if sys_bits != 32:
267
+ raise AssertionError('python must be 32-bit')
268
+
269
+ # Build.
270
+ self.send_port = int(send_port)
271
+ self.receive_port = int(receive_port)
272
+ self.echo = echo
273
+ self.loader: WeChatHookLoader | None = None
274
+ self._injected: bool = False
275
+ self._inject_data: dict | None = None
276
+ self._logined: bool = False
277
+ self._login_data: dict | None = None
278
+
279
+ def inject(self) -> bool:
280
+ """
281
+ Inject WeChat hook.
282
+
283
+ Returns
284
+ -------
285
+ Whether it is successful.
286
+ """
287
+
288
+ # Check.
289
+ if self._injected:
290
+ send_socket(
291
+ '127.0.0.1',
292
+ self.receive_port,
293
+ {
294
+ 'type': 11024,
295
+ 'data': self._inject_data
296
+ }
297
+ )
298
+ if self._logined:
299
+ send_socket(
300
+ '127.0.0.1',
301
+ self.receive_port,
302
+ {
303
+ 'type': 11025,
304
+ 'data': self._login_data
305
+ }
306
+ )
307
+ return
308
+
309
+ # Start.
310
+ self.loader = WeChatHookLoader()
311
+ result = bool(self.loader.inject())
312
+ self._injected = True
313
+
314
+ return result
315
+
316
+ def register_callback(
317
+ self,
318
+ callback: Callable[[int, int, Dict[str, Any]], None],
319
+ ) -> None:
320
+ """
321
+ Register message callback.
322
+
323
+ Parameters
324
+ ----------
325
+ callback : Callback function.
326
+ """
327
+
328
+ # Register.
329
+ global _recv_callback
330
+ _recv_callback = callback
331
+
332
+ def send_payload(self, payload: Dict[str, Any]) -> bool:
333
+ """
334
+ Send raw protocol payload.
335
+
336
+ Parameters
337
+ ----------
338
+ payload : WeChat protocol JSON.
339
+
340
+ Returns
341
+ -------
342
+ Whether it is successful.
343
+ """
344
+
345
+ # Check.
346
+ if not self.loader or not _socket_id:
347
+ return False
348
+
349
+ # Send.
350
+ result = self.loader.send(
351
+ _socket_id,
352
+ json_dumps(payload, ensure_ascii=False),
353
+ )
354
+
355
+ return result
356
+
357
+ def start_socket(self) -> None:
358
+ """
359
+ Start socket, will block.
360
+ """
361
+
362
+ # Receive.
363
+ def callback(_, request_type: int, request_data: dict[str, str | int]) -> None:
364
+ if self.echo:
365
+ print(f'\n{request_type} | {request_data}')
366
+ if request_type == 11024:
367
+ self._inject_data = request_data
368
+ self._injected = True
369
+ elif request_type == 11025:
370
+ self._login_data = request_data
371
+ self._logined = True
372
+ send_socket(
373
+ '127.0.0.1',
374
+ self.receive_port,
375
+ {
376
+ 'type': request_type,
377
+ 'data': request_data
378
+ }
379
+ )
380
+ self.register_callback(callback)
381
+
382
+ # Send.
383
+ def handler(data: Literal['inject'] | Dict[str, Any]) -> None:
384
+ if data == 'inject':
385
+ self.inject()
386
+ else:
387
+ self.send_payload(data)
388
+ print(f'start listening on port {self.send_port}')
389
+ listen_socket(
390
+ '127.0.0.1',
391
+ self.send_port,
392
+ handler
393
+ )
@@ -0,0 +1,8 @@
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "reywechat-hook"
7
+ version = "1.0.1"
8
+ source = { editable = "." }