wechat-cli 0.1.0__py3-none-any.whl
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.
- wechat_cli/__init__.py +3 -0
- wechat_cli/__main__.py +3 -0
- wechat_cli/assets/app.js +79 -0
- wechat_cli/assets/bridge.cpp +643 -0
- wechat_cli/assets/controls.swift +218 -0
- wechat_cli/assets/entitlements.plist +38 -0
- wechat_cli/assets/favicon.svg +1 -0
- wechat_cli/assets/index.html +10 -0
- wechat_cli/assets/native-agent.js +334 -0
- wechat_cli/assets/pdf-thumbnail.swift +13 -0
- wechat_cli/assets/profile.json +232 -0
- wechat_cli/assets/profiles/4.1.13-269631.json +182 -0
- wechat_cli/assets/profiles/4.1.13.34-269602.json +232 -0
- wechat_cli/assets/profiles/4.1.15-270135.json +194 -0
- wechat_cli/assets/startup-notice.js +78 -0
- wechat_cli/assets/style.css +1 -0
- wechat_cli/cli.py +238 -0
- wechat_cli/client.py +59 -0
- wechat_cli/codec.py +14 -0
- wechat_cli/config.py +84 -0
- wechat_cli/controls.py +223 -0
- wechat_cli/errors.py +7 -0
- wechat_cli/idempotency.py +82 -0
- wechat_cli/lifecycle.py +141 -0
- wechat_cli/media.py +341 -0
- wechat_cli/models.py +260 -0
- wechat_cli/native.py +229 -0
- wechat_cli/network.py +9 -0
- wechat_cli/server.py +518 -0
- wechat_cli/setup.py +342 -0
- wechat_cli/worker.py +227 -0
- wechat_cli-0.1.0.dist-info/METADATA +178 -0
- wechat_cli-0.1.0.dist-info/RECORD +36 -0
- wechat_cli-0.1.0.dist-info/WHEEL +4 -0
- wechat_cli-0.1.0.dist-info/entry_points.txt +2 -0
- wechat_cli-0.1.0.dist-info/licenses/LICENSE +7 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "4.1.15",
|
|
3
|
+
"build": "270135",
|
|
4
|
+
"arch": "arm64",
|
|
5
|
+
"payloadSha256": "5f5a81c4d8b000dec1c7203738fc4dba75ce752b55874fc1b6b5869f9dfb1027",
|
|
6
|
+
"taskContextTlsOffset": "0xa24de80",
|
|
7
|
+
"sessionServiceMarkerOffset": "0x9da18d0",
|
|
8
|
+
"voiceFrontDescriptorOffset": "0x9c03d80",
|
|
9
|
+
"enumeration": {
|
|
10
|
+
"offset": "0x2be0d64",
|
|
11
|
+
"signature": "fa67bba9f85f01a9f65702a9f44f03a9fd7b04a9fd030191ff0308d1f30308aa",
|
|
12
|
+
"scanPattern": "fa 67 bb a9 f8 5f 01 a9 f6 57 02 a9 f4 4f 03 a9 fd 7b 04 a9 fd 03 01 91 ff 03 08 d1 f3 03 08 aa f5 c3 02 91 a8 7a 03 d0 08 4d 46 f9 08 01 40 f9 a8 83 1b f8 fb 7b 15 95 f4 03 00 aa 00 e4 00 6f 60 02 00 ad 08 f0 a7 d2 48 55 d5 f2 48 55 f5 f2 68 12 00 f9 40 e5 05 4f a0 02 00 ad a8 aa ca 92 48 55 f5 f2 e8 6b 00 f9 6a 22 6a 94 08 00 40 f9 09 35 40 f9 e8 63 03 91 20 01 3f d6 e0 6f 40 f9 : ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 1f 00 00 9f ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 fc ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff 00 00 00 fc ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff"
|
|
13
|
+
},
|
|
14
|
+
"initialMessages": {
|
|
15
|
+
"offset": "0x31c0484",
|
|
16
|
+
"signature": "fc6fbaa9fa6701a9f85f02a9f65703a9f44f04a9fd7b05a9fd430191ff830cd1"
|
|
17
|
+
},
|
|
18
|
+
"pagedMessages": {
|
|
19
|
+
"offset": "0x31c15e4",
|
|
20
|
+
"signature": "fc6fbaa9fa6701a9f85f02a9f65703a9f44f04a9fd7b05a9fd430191ff430cd1"
|
|
21
|
+
},
|
|
22
|
+
"mapNodeDestructor": {
|
|
23
|
+
"offset": "0x2be59e0",
|
|
24
|
+
"signature": "c10400b4f44fbea9fd7b01a9fd430091f30301aa06000014e00313aa010b8052"
|
|
25
|
+
},
|
|
26
|
+
"serviceRegistry": {
|
|
27
|
+
"offset": "0x44e6b1c",
|
|
28
|
+
"signature": "ffc302d1fa6706a9f85f07a9f65708a9f44f09a9fd7b0aa9fd830291f40301aa"
|
|
29
|
+
},
|
|
30
|
+
"cachedMessageService": {
|
|
31
|
+
"offset": "0x454182c",
|
|
32
|
+
"signature": "ff8301d1f85f02a9f65703a9f44f04a9fd7b05a9fd430191f40300aaf30308aa"
|
|
33
|
+
},
|
|
34
|
+
"createMessageServiceTask": {
|
|
35
|
+
"offset": "0x666ab58",
|
|
36
|
+
"signature": "ff4301d1f44f03a9fd7b04a9fd030191f40300aaf30308aae8f301b2e8a301a9"
|
|
37
|
+
},
|
|
38
|
+
"messageUiConversion": {
|
|
39
|
+
"offset": "0x31f808c",
|
|
40
|
+
"signature": "fc6fbaa9fa6701a9f85f02a9f65703a9f44f04a9fd7b05a9fd430191ff8315d1"
|
|
41
|
+
},
|
|
42
|
+
"messageUiDestructor": {
|
|
43
|
+
"offset": "0x396780",
|
|
44
|
+
"signature": "f44fbea9fd7b01a9fd430091f30300aa009841f9c00000b4140040f901038052"
|
|
45
|
+
},
|
|
46
|
+
"sendInfoConstructor": {
|
|
47
|
+
"offset": "0x69a714",
|
|
48
|
+
"signature": "f44fbea9fd7b01a9fd430091f40301aaf30300aae003679e0004186e0080803c"
|
|
49
|
+
},
|
|
50
|
+
"sendResultDestructor": {
|
|
51
|
+
"offset": "0x3189a68",
|
|
52
|
+
"signature": "f657bda9f44f01a9fd7b02a9fd830091f30300aa141040f9d40300b4751640f9"
|
|
53
|
+
},
|
|
54
|
+
"sendFailureCtor": {
|
|
55
|
+
"offset": "0x30f6640",
|
|
56
|
+
"signature": "1f0000391f00013901080929c0035fd6ff8302d1fa6705a9f85f06a9f65707a9"
|
|
57
|
+
},
|
|
58
|
+
"sessionService": {
|
|
59
|
+
"offset": "0x44e6b1c",
|
|
60
|
+
"signature": "ffc302d1fa6706a9f85f07a9f65708a9f44f09a9fd7b0aa9fd830291f40301aa"
|
|
61
|
+
},
|
|
62
|
+
"sessionDestructor": {
|
|
63
|
+
"offset": "0x1ef570",
|
|
64
|
+
"signature": "f657bda9f44f01a9fd7b02a9fd830091f30300aa085cc839c802f83768dec739"
|
|
65
|
+
},
|
|
66
|
+
"isMinimizedSession": {
|
|
67
|
+
"offset": "0x4d64628",
|
|
68
|
+
"signature": "f44fbea9fd7b01a9fd430091f30300aacf3cf99780000034683e41b908010412"
|
|
69
|
+
},
|
|
70
|
+
"makeUiSendInfo": {
|
|
71
|
+
"offset": "0x6f3990",
|
|
72
|
+
"signature": "f44fbea9fd7b01a9fd430091f30308aa008a8052a331a995e003679e0004186e"
|
|
73
|
+
},
|
|
74
|
+
"voiceAudioPath": {
|
|
75
|
+
"offset": "0x35c18f8",
|
|
76
|
+
"signature": "ff4303d1f6570aa9f44f0ba9fd7b0ca9fd030391f50302aaf40300aaf30308aa"
|
|
77
|
+
},
|
|
78
|
+
"voiceDataGetter": {
|
|
79
|
+
"offset": "0x31c4144",
|
|
80
|
+
"signature": "ff8302d1f65707a9f44f08a9fd7b09a9fd430291f30302aaf60301aaf40300aa"
|
|
81
|
+
},
|
|
82
|
+
"voiceTranscriptionStart": {
|
|
83
|
+
"offset": "0x412eb70",
|
|
84
|
+
"signature": "09fc40f9200540f99b200214fc6fbda9f44f01a9fd7b02a9fd830091ff030fd1"
|
|
85
|
+
},
|
|
86
|
+
"ocrLocalRecognition": {
|
|
87
|
+
"offset": "0x36ef0c4",
|
|
88
|
+
"signature": "ff0304d1fa670ba9f85f0ca9f6570da9f44f0ea9fd7b0fa9fdc30391f60302aa"
|
|
89
|
+
},
|
|
90
|
+
"makeVoiceSendInfo": {
|
|
91
|
+
"offset": "0x448a844",
|
|
92
|
+
"signature": "fa67bba9f85f01a9f65702a9f44f03a9fd7b04a9fd030191f50304aaf60303aa"
|
|
93
|
+
},
|
|
94
|
+
"dictationStart": {
|
|
95
|
+
"offset": "0x1d07c20",
|
|
96
|
+
"signature": "ff4306d1fc6f14a9f85f15a9f65716a9f44f17a9fd7b18a9fd030691f7c30291"
|
|
97
|
+
},
|
|
98
|
+
"migrationStopServices": {
|
|
99
|
+
"offset": "0x42a5ed0",
|
|
100
|
+
"signature": "ffc301d1f44f05a9fd7b06a9fd830191250e0f94080040f9093540f9e8230091"
|
|
101
|
+
},
|
|
102
|
+
"migrationCancelTransfer": {
|
|
103
|
+
"offset": "0x42a5ed0",
|
|
104
|
+
"signature": "ffc301d1f44f05a9fd7b06a9fd830191250e0f94080040f9093540f9e8230091"
|
|
105
|
+
},
|
|
106
|
+
"isPinnedSession": {
|
|
107
|
+
"offset": "0x4d6461c",
|
|
108
|
+
"signature": "08f44439000d0353c0035fd6f44fbea9fd7b01a9fd430091f30300aacf3cf997"
|
|
109
|
+
},
|
|
110
|
+
"messageFormatter": {
|
|
111
|
+
"offset": "0x4cbe4cc",
|
|
112
|
+
"signature": "ffc301d1f44f05a9fd7b06a9fd830191f40300aaf30308aae8630091d6ffff97"
|
|
113
|
+
},
|
|
114
|
+
"accountFactory": {
|
|
115
|
+
"offset": "0x466aca8",
|
|
116
|
+
"signature": "ffc300d1f44f01a9fd7b02a9fd830091f30308aa88ef02f0009d47f9080040f9"
|
|
117
|
+
},
|
|
118
|
+
"messageService": {
|
|
119
|
+
"offset": "0x268f370",
|
|
120
|
+
"signature": "ffc301d1f44f05a9fd7b06a9fd830191f40300aaf30308aa88b803f008010091"
|
|
121
|
+
},
|
|
122
|
+
"messageServiceResult": {
|
|
123
|
+
"offset": "0x21b3684",
|
|
124
|
+
"signature": "f44fbea9fd7b01a9fd430091000040f96cdc1295200300b4f30300aa000040f9"
|
|
125
|
+
},
|
|
126
|
+
"messageTaskResult": {
|
|
127
|
+
"offset": "0x3206474",
|
|
128
|
+
"signature": "f44fbea9fd7b01a9fd430091000040f9f090d194200300b4f30300aa000040f9"
|
|
129
|
+
},
|
|
130
|
+
"taskContextGuard": {
|
|
131
|
+
"offset": "0x6676adc",
|
|
132
|
+
"signature": "f44fbea9fd7b01a9fd430091f30300aaa0de01f000003a91080040f900013fd6"
|
|
133
|
+
},
|
|
134
|
+
"contactDisplayName": {
|
|
135
|
+
"offset": "0x2ac697c",
|
|
136
|
+
"signature": "ff0307d1f65719a9f44f1aa9fd7b1ba9fdc30691f50302aaf40301aaf30308aa"
|
|
137
|
+
},
|
|
138
|
+
"makeNativeUuid": {
|
|
139
|
+
"offset": "0x33e1c",
|
|
140
|
+
"signature": "ff0302d1f44f06a9fd7b07a9fdc30191f30308aa08d804f0084d46f9080140f9"
|
|
141
|
+
},
|
|
142
|
+
"sendInfoClone": {
|
|
143
|
+
"offset": "0x4d10630",
|
|
144
|
+
"signature": "f657bda9f44f01a9fd7b02a9fd830091f50301aaf40308aa00bc805279be9094"
|
|
145
|
+
},
|
|
146
|
+
"sendInfoDestructor": {
|
|
147
|
+
"offset": "0x69b42c",
|
|
148
|
+
"signature": "f44fbea9fd7b01a9fd430091f30300aaa8c004f00801199108410091080000f9"
|
|
149
|
+
},
|
|
150
|
+
"sendGroupMessages": {
|
|
151
|
+
"offset": "0x3183c08",
|
|
152
|
+
"signature": "e923b96dfc6f01a9fa6702a9f85f03a9f65704a9f44f05a9fd7b06a9fd830191"
|
|
153
|
+
},
|
|
154
|
+
"sendStageCtor": {
|
|
155
|
+
"offset": "0x304f1cc",
|
|
156
|
+
"signature": "f657bda9f44f01a9fd7b02a9fd830091f30300aa0108002900e4006f0080803c"
|
|
157
|
+
},
|
|
158
|
+
"sendContextConstructor": {
|
|
159
|
+
"offset": "0x30ef9e0",
|
|
160
|
+
"signature": "ff8301d1fa6701a9f85f02a9f65703a9f44f04a9fd7b05a9fd430191f90302aa"
|
|
161
|
+
},
|
|
162
|
+
"copySessions": {
|
|
163
|
+
"offset": "0x27f5434",
|
|
164
|
+
"signature": "f657bda9f44f01a9fd7b02a9fd830091f40301aaf30300aa00e4006f000000ad"
|
|
165
|
+
},
|
|
166
|
+
"sendProcessor": {
|
|
167
|
+
"offset": "0x319a6a8",
|
|
168
|
+
"signature": "f85fbca9f65701a9f44f02a9fd7b03a9fdc30091ff0309d1f40302aaf50301aa"
|
|
169
|
+
},
|
|
170
|
+
"voiceFrontRegistry": {
|
|
171
|
+
"offset": "0x456f244",
|
|
172
|
+
"signature": "ffc307d1fa671aa9f85f1ba9f6571ca9f44f1da9fd7b1ea9fd830791f30301aa"
|
|
173
|
+
},
|
|
174
|
+
"ocrService": {
|
|
175
|
+
"offset": "0x21e9898",
|
|
176
|
+
"signature": "ffc301d1f44f05a9fd7b06a9fd830191f40300aaf30308aae8dc039008210191"
|
|
177
|
+
},
|
|
178
|
+
"ocrInitialize": {
|
|
179
|
+
"offset": "0x36ec308",
|
|
180
|
+
"signature": "ffc306d1f65718a9f44f19a9fd7b1aa9fd830691f30300aab4a302d1482203d0"
|
|
181
|
+
},
|
|
182
|
+
"accountServices": {
|
|
183
|
+
"offset": "0x17e9ca4",
|
|
184
|
+
"signature": "0aa442a90a2500a9890000b428210091"
|
|
185
|
+
},
|
|
186
|
+
"contactService": {
|
|
187
|
+
"offset": "0x230000c",
|
|
188
|
+
"signature": "ffc301d1f44f05a9fd7b06a9fd830191"
|
|
189
|
+
},
|
|
190
|
+
"voiceTranscriptionAttach": {
|
|
191
|
+
"offset": "0x21d834",
|
|
192
|
+
"signature": "ff0303d1f85f08a9f65709a9f44f0aa9fd7b0ba9fdc30291f30304aaf40303aa"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Suppress only WeChat's installation-location notice. Installed before Gadget resumes main().
|
|
2
|
+
// Returning the normal OK response lets WeChat persist its own suppression preference.
|
|
3
|
+
(() => {
|
|
4
|
+
const policy = globalThis.WECHAT_INSTALL_NOTICE = {installed: false, suppressed: 0};
|
|
5
|
+
const keep = [];
|
|
6
|
+
policy.callbacks = keep;
|
|
7
|
+
try {
|
|
8
|
+
const exported = name => Module.getGlobalExportByName(name);
|
|
9
|
+
const getClass = new NativeFunction(exported('objc_getClass'), 'pointer', ['pointer']);
|
|
10
|
+
const getSelector = new NativeFunction(exported('sel_registerName'), 'pointer', ['pointer']);
|
|
11
|
+
const instanceMethod = new NativeFunction(exported('class_getInstanceMethod'), 'pointer', ['pointer', 'pointer']);
|
|
12
|
+
const implementation = new NativeFunction(exported('method_getImplementation'), 'pointer', ['pointer']);
|
|
13
|
+
const msg = new NativeFunction(exported('objc_msgSend'), 'pointer', ['pointer', 'pointer']);
|
|
14
|
+
const setState = new NativeFunction(exported('objc_msgSend'), 'void', ['pointer', 'pointer', 'long']);
|
|
15
|
+
const selector = name => getSelector(Memory.allocUtf8String(name));
|
|
16
|
+
const selectors = Object.fromEntries(['messageText', 'informativeText', 'UTF8String', 'suppressionButton'].map(name => [name, selector(name)]));
|
|
17
|
+
const stateSelector = selector('setState:');
|
|
18
|
+
const alertClass = getClass(Memory.allocUtf8String('NSAlert'));
|
|
19
|
+
if (alertClass.isNull()) return;
|
|
20
|
+
function text(object, field) {
|
|
21
|
+
const string = msg(object, selectors[field]);
|
|
22
|
+
if (string.isNull()) return '';
|
|
23
|
+
const bytes = msg(string, selectors.UTF8String);
|
|
24
|
+
return bytes.isNull() ? '' : bytes.readUtf8String();
|
|
25
|
+
}
|
|
26
|
+
function matches(alert) {
|
|
27
|
+
try {
|
|
28
|
+
const title = text(alert, 'messageText').replace(/Weixin/g, 'WeChat').trim();
|
|
29
|
+
const body = text(alert, 'informativeText').replace(/Weixin/g, 'WeChat').trim();
|
|
30
|
+
const english = title === 'WeChat was opened from the installation package'
|
|
31
|
+
&& body === 'We recommend you move WeChat to the Applications folder before using it.';
|
|
32
|
+
const chinese = ['微信从安装包中打开', '微信是从安装包中打开的', '微信从安装包打开'].includes(title)
|
|
33
|
+
&& /建议.*微信.*应用程序/.test(body);
|
|
34
|
+
if (!english && !chinese) return false;
|
|
35
|
+
const checkbox = msg(alert, selectors.suppressionButton);
|
|
36
|
+
if (!checkbox.isNull()) setState(checkbox, stateSelector, 1);
|
|
37
|
+
policy.suppressed++;
|
|
38
|
+
return true;
|
|
39
|
+
} catch (_) { return false; }
|
|
40
|
+
}
|
|
41
|
+
function method(name) {
|
|
42
|
+
const value = instanceMethod(alertClass, selector(name));
|
|
43
|
+
return value.isNull() ? null : implementation(value);
|
|
44
|
+
}
|
|
45
|
+
const modal = method('runModal');
|
|
46
|
+
if (modal) {
|
|
47
|
+
const original = new NativeFunction(modal, 'long', ['pointer', 'pointer']);
|
|
48
|
+
const replacement = new NativeCallback((self, cmd) => matches(self) ? 1000 : original(self, cmd),
|
|
49
|
+
'long', ['pointer', 'pointer']);
|
|
50
|
+
Interceptor.replace(modal, replacement); keep.push(replacement, original);
|
|
51
|
+
}
|
|
52
|
+
const sheet = method('beginSheetModalForWindow:completionHandler:');
|
|
53
|
+
if (sheet) {
|
|
54
|
+
const original = new NativeFunction(sheet, 'void', ['pointer', 'pointer', 'pointer', 'pointer']);
|
|
55
|
+
const copyBlock = new NativeFunction(exported('_Block_copy'), 'pointer', ['pointer']);
|
|
56
|
+
const releaseBlock = new NativeFunction(exported('_Block_release'), 'void', ['pointer']);
|
|
57
|
+
const queueGetter = Module.findGlobalExportByName('dispatch_get_main_queue');
|
|
58
|
+
const mainQueue = queueGetter ? new NativeFunction(queueGetter, 'pointer', []) : () => exported('_dispatch_main_q');
|
|
59
|
+
const dispatch = new NativeFunction(exported('dispatch_async_f'), 'void', ['pointer', 'pointer', 'pointer']);
|
|
60
|
+
const complete = new NativeCallback(block => {
|
|
61
|
+
try {
|
|
62
|
+
const callback = new NativeFunction(block.add(16).readPointer(), 'void', ['pointer', 'long']);
|
|
63
|
+
callback(block, 1000);
|
|
64
|
+
} finally { releaseBlock(block); }
|
|
65
|
+
}, 'void', ['pointer']);
|
|
66
|
+
const replacement = new NativeCallback((self, cmd, window, block) => {
|
|
67
|
+
if (!matches(self)) return original(self, cmd, window, block);
|
|
68
|
+
if (!block.isNull()) dispatch(mainQueue(), copyBlock(block), complete);
|
|
69
|
+
}, 'void', ['pointer', 'pointer', 'pointer', 'pointer']);
|
|
70
|
+
Interceptor.replace(sheet, replacement); keep.push(complete, replacement, original);
|
|
71
|
+
}
|
|
72
|
+
policy.installed = Boolean(modal || sheet);
|
|
73
|
+
// Keep callbacks alive for the lifetime of this script.
|
|
74
|
+
policy.callbacks = keep;
|
|
75
|
+
} catch (_) {
|
|
76
|
+
// If AppKit differs, preserve normal app behavior instead of dismissing an unknown alert.
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{font:15px system-ui,-apple-system,sans-serif;color:#192d28;background:#f6f8f7}*{box-sizing:border-box}body{margin:0}header{background:#163f34;color:white;padding:18px 26px;display:flex;align-items:center;gap:12px}header div{flex:1}header strong{font-size:22px}header span{display:block;opacity:.7;font-size:12px;margin-top:4px}button,input,textarea,select{font:inherit}button{background:white;border:1px solid #c7d7d0;border-radius:6px;padding:8px 12px;cursor:pointer;color:#163f34}button:hover{background:#edf5f0}button:disabled{opacity:.5;cursor:default}button:focus-visible,input:focus,textarea:focus{outline:2px solid #298667;outline-offset:2px}#status{padding:12px 26px;margin:0;min-height:46px;white-space:pre-wrap;border-bottom:1px solid #dce4df;font-size:13px}main{display:grid;grid-template-columns:300px 1fr;min-height:70vh}aside{padding:18px;border-right:1px solid #dce4df}label{display:block;font-size:13px;font-weight:600;margin-bottom:8px}input[type=search],textarea{width:100%;padding:10px;border:1px solid #c7d7d0;border-radius:6px;background:white}nav{display:flex;flex-direction:column;gap:6px;margin-top:14px;max-height:75vh;overflow:auto}nav button{text-align:left;padding:12px}nav button[aria-current=true]{background:#dceee4;border-color:#298667}nav small{display:block;color:#65776d;margin-top:5px;overflow-wrap:anywhere}.conversation-title{display:flex;align-items:center;gap:12px;padding:0 0 18px}h1{flex:1;font-size:20px;margin:0}.conversation-title label{margin:0}section{padding:22px;min-width:0}#messages{display:flex;flex-direction:column;gap:12px;max-height:60vh;overflow:auto}article{background:white;border:1px solid #dce4df;border-radius:8px;padding:14px;overflow-wrap:anywhere}article small{color:#65776d}article p{white-space:pre-wrap;line-height:1.6}article button{margin:4px 6px 0 0}article img,article video{max-width:100%;max-height:400px;display:block;margin-top:12px}article audio{display:block;margin-top:12px}form{border-top:1px solid #dce4df;padding-top:16px;margin-top:20px}#compose button{display:block;margin:10px 0 0 auto;background:#1b6d50;color:white}#upload{display:flex;flex-wrap:wrap;gap:8px;align-items:center}#upload label{width:100%}footer{padding:16px 26px;color:#65776d;font-size:12px}@media(max-width:750px){main{grid-template-columns:1fr}aside{border-right:0;border-bottom:1px solid #dce4df}nav{max-height:220px}.conversation-title{flex-wrap:wrap}header{padding:14px}section{padding:16px}}
|
wechat_cli/cli.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""uvx wechat-cli: lifecycle commands and API subcommands with JSON output."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
import webbrowser
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from urllib.parse import urlencode
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .client import ApiClient
|
|
15
|
+
from .config import data_directory
|
|
16
|
+
from .errors import AppError
|
|
17
|
+
from .lifecycle import configured_port, ensure_server, health, serve
|
|
18
|
+
from .setup import WECHAT_URL, doctor, setup
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def port_number(value):
|
|
22
|
+
try:
|
|
23
|
+
port = int(value)
|
|
24
|
+
if not 1 <= port <= 65535:
|
|
25
|
+
raise ValueError()
|
|
26
|
+
return port
|
|
27
|
+
except ValueError:
|
|
28
|
+
raise argparse.ArgumentTypeError("port must be between 1 and 65535") from None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parser():
|
|
32
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
33
|
+
common.add_argument(
|
|
34
|
+
"--data-dir", default=argparse.SUPPRESS, help="Persistent app, token and service directory"
|
|
35
|
+
)
|
|
36
|
+
common.add_argument(
|
|
37
|
+
"--port", type=port_number, default=argparse.SUPPRESS, help="Local API port (default: 8787)"
|
|
38
|
+
)
|
|
39
|
+
root = argparse.ArgumentParser(
|
|
40
|
+
prog="wechat-cli",
|
|
41
|
+
parents=[common],
|
|
42
|
+
description="Python CLI, local API and web interface for macOS WeChat",
|
|
43
|
+
)
|
|
44
|
+
root.add_argument("--version", action="version", version=__version__)
|
|
45
|
+
commands = root.add_subparsers(dest="command")
|
|
46
|
+
|
|
47
|
+
def add(name, description):
|
|
48
|
+
return commands.add_parser(name, help=description, description=description, parents=[common])
|
|
49
|
+
|
|
50
|
+
init = add("setup", "Download and patch a separate compatible WeChat copy, or register an existing one")
|
|
51
|
+
sources = init.add_mutually_exclusive_group()
|
|
52
|
+
sources.add_argument("--app", help="Register an already patched app")
|
|
53
|
+
sources.add_argument("--source", help="Copy and patch a local stock WeChat.app")
|
|
54
|
+
init.add_argument("--destination", help="New patched app location (must not already exist)")
|
|
55
|
+
init.add_argument("--gadget", help="Use a local Frida Gadget dylib instead of downloading it")
|
|
56
|
+
init.add_argument(
|
|
57
|
+
"--wechat-url", default=WECHAT_URL, help="HTTPS installer URL (payload must be supported)"
|
|
58
|
+
)
|
|
59
|
+
add("doctor", "Check app, payload, Gadget and Frida installation")
|
|
60
|
+
start = add("start", "Start the patched WeChat and persistent local API")
|
|
61
|
+
start.add_argument("--foreground", action="store_true", help="Keep the service in this terminal")
|
|
62
|
+
add("serve", "Run WeChat and the API in the foreground")
|
|
63
|
+
add("stop", "Stop the local API; keep WeChat signed in and running")
|
|
64
|
+
web = add("web", "Start the service and open its web interface")
|
|
65
|
+
web.add_argument("--no-open", action="store_true", help="Print the URL without opening a browser")
|
|
66
|
+
add("status", "Read native WeChat connection status")
|
|
67
|
+
add("chats", "List native conversations")
|
|
68
|
+
messages = add("messages", "Read recent messages, newest first")
|
|
69
|
+
messages.add_argument("chat_id")
|
|
70
|
+
messages.add_argument("--limit", type=int, choices=range(1, 101), default=50, metavar="1..100")
|
|
71
|
+
send = add("send", "Send a text message")
|
|
72
|
+
send.add_argument("chat_id")
|
|
73
|
+
send.add_argument("text")
|
|
74
|
+
send.add_argument(
|
|
75
|
+
"--dry-run", action="store_true", help="Validate native text preparation without sending"
|
|
76
|
+
)
|
|
77
|
+
send.add_argument("--key", help="Reuse this idempotency key for retries (retained for 24 hours)")
|
|
78
|
+
for name in ["send-file", "send-voice"]:
|
|
79
|
+
command = add(
|
|
80
|
+
name, "Send a local file" if name == "send-file" else "Send SILK, mono PCM WAV or raw PCM audio"
|
|
81
|
+
)
|
|
82
|
+
command.add_argument("chat_id")
|
|
83
|
+
command.add_argument("file", type=Path)
|
|
84
|
+
command.add_argument("--key")
|
|
85
|
+
if name == "send-voice":
|
|
86
|
+
command.add_argument("--format", choices=["silk", "wav", "pcm_s16le"], default="wav")
|
|
87
|
+
command.add_argument("--sample-rate", type=int)
|
|
88
|
+
for name, description in [
|
|
89
|
+
("media", "Download media for a recent message"),
|
|
90
|
+
("ocr", "Extract text from an image"),
|
|
91
|
+
("transcribe", "Start WeChat voice-to-text"),
|
|
92
|
+
("transcript", "Read voice-to-text status"),
|
|
93
|
+
("download", "Ask the active WeChat chat to download an attachment"),
|
|
94
|
+
]:
|
|
95
|
+
command = add(name, description)
|
|
96
|
+
command.add_argument("chat_id")
|
|
97
|
+
command.add_argument("message_id", help="Exact string ID from messages")
|
|
98
|
+
if name == "media":
|
|
99
|
+
command.add_argument("--output", required=True, type=Path)
|
|
100
|
+
command.add_argument("--variant", choices=["full", "preview"], default="full")
|
|
101
|
+
if name in {"transcribe", "download"}:
|
|
102
|
+
command.add_argument("--key")
|
|
103
|
+
request = add("api", "Call any local /api/ route")
|
|
104
|
+
request.add_argument("path")
|
|
105
|
+
request.add_argument("--method", choices=["GET", "POST", "DELETE", "PUT", "PATCH"], default="GET")
|
|
106
|
+
request.add_argument("--json", dest="body", help="JSON object body")
|
|
107
|
+
request.add_argument("--key")
|
|
108
|
+
request.add_argument("--output", type=Path, help="Save the response as binary")
|
|
109
|
+
return root
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def upload(path):
|
|
113
|
+
if path.stat().st_size > 8 * 1024**2:
|
|
114
|
+
raise AppError("file_too_large", "CLI uploads must be at most 8 MiB", 413)
|
|
115
|
+
return base64.b64encode(path.read_bytes()).decode()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def run(args):
|
|
119
|
+
directory = data_directory(getattr(args, "data_dir", None))
|
|
120
|
+
command = args.command or "start"
|
|
121
|
+
port = configured_port(directory, getattr(args, "port", None))
|
|
122
|
+
if command == "setup":
|
|
123
|
+
if args.app and (args.destination or args.gadget):
|
|
124
|
+
raise AppError("invalid_options", "--destination and --gadget apply to preparing a new copy")
|
|
125
|
+
return setup(
|
|
126
|
+
directory,
|
|
127
|
+
app=args.app,
|
|
128
|
+
source=args.source,
|
|
129
|
+
destination=args.destination,
|
|
130
|
+
gadget=args.gadget,
|
|
131
|
+
wechat_url=args.wechat_url,
|
|
132
|
+
)
|
|
133
|
+
if command == "doctor":
|
|
134
|
+
return doctor(directory)
|
|
135
|
+
if command == "serve" or command == "start" and getattr(args, "foreground", False):
|
|
136
|
+
return serve(directory, port)
|
|
137
|
+
if command == "stop":
|
|
138
|
+
result = ApiClient(directory, port).request("/api/server/stop", method="POST", data={})
|
|
139
|
+
for _ in range(100):
|
|
140
|
+
try:
|
|
141
|
+
health(directory, port)
|
|
142
|
+
except AppError:
|
|
143
|
+
return result
|
|
144
|
+
time.sleep(0.05)
|
|
145
|
+
raise AppError("server_stop_timeout", "Service shutdown is still in progress", 504)
|
|
146
|
+
ensure_server(directory, port)
|
|
147
|
+
if command in {"start", "web"}:
|
|
148
|
+
url = f"http://127.0.0.1:{port}/"
|
|
149
|
+
if command == "web" and not args.no_open:
|
|
150
|
+
webbrowser.open(url)
|
|
151
|
+
return {"running": True, "url": url, "tokenFile": str(directory / "api-token")}
|
|
152
|
+
client = ApiClient(directory, port)
|
|
153
|
+
if command == "status":
|
|
154
|
+
return client.request("/api/native/status")
|
|
155
|
+
if command == "chats":
|
|
156
|
+
return client.chats()
|
|
157
|
+
if command == "messages":
|
|
158
|
+
return client.messages(args.chat_id, args.limit)
|
|
159
|
+
key = getattr(args, "key", None)
|
|
160
|
+
mutation = (
|
|
161
|
+
command in {"send", "send-file", "send-voice", "transcribe", "download"}
|
|
162
|
+
or command == "api"
|
|
163
|
+
and args.method == "POST"
|
|
164
|
+
)
|
|
165
|
+
mutation = mutation and not (command == "send" and args.dry_run)
|
|
166
|
+
if mutation:
|
|
167
|
+
key = key or str(uuid.uuid4())
|
|
168
|
+
print(f"Idempotency-Key: {key} (reuse --key for retries)", file=sys.stderr)
|
|
169
|
+
if command == "send" and args.dry_run:
|
|
170
|
+
return client.request(
|
|
171
|
+
"/api/messages/prepare", method="POST", data={"chatId": args.chat_id, "text": args.text}
|
|
172
|
+
)
|
|
173
|
+
if command == "send":
|
|
174
|
+
return client.send(args.chat_id, args.text, idempotency_key=key)
|
|
175
|
+
if command in {"send-file", "send-voice"}:
|
|
176
|
+
input = {"chatId": args.chat_id, "dataBase64": upload(args.file)}
|
|
177
|
+
if command == "send-file":
|
|
178
|
+
input["name"] = args.file.name
|
|
179
|
+
else:
|
|
180
|
+
input["format"] = args.format
|
|
181
|
+
if args.sample_rate is not None:
|
|
182
|
+
input["sampleRate"] = args.sample_rate
|
|
183
|
+
return client.request(
|
|
184
|
+
"/api/messages/file" if command == "send-file" else "/api/messages/voice",
|
|
185
|
+
method="POST",
|
|
186
|
+
data=input,
|
|
187
|
+
key=key,
|
|
188
|
+
)
|
|
189
|
+
if command in {"media", "ocr", "transcribe", "transcript", "download"}:
|
|
190
|
+
input = {"chatId": args.chat_id, "id": args.message_id}
|
|
191
|
+
paths = {
|
|
192
|
+
"media": "/api/native/media",
|
|
193
|
+
"ocr": "/api/native/images/text",
|
|
194
|
+
"transcribe": "/api/native/voice/transcribe",
|
|
195
|
+
"transcript": "/api/native/voice/transcript",
|
|
196
|
+
"download": "/api/native/attachments/download",
|
|
197
|
+
}
|
|
198
|
+
if command in {"media", "transcript"}:
|
|
199
|
+
if command == "media":
|
|
200
|
+
input["variant"] = args.variant
|
|
201
|
+
result = client.request(paths[command] + "?" + urlencode(input), binary=command == "media")
|
|
202
|
+
if command == "media":
|
|
203
|
+
with args.output.open("xb") as output:
|
|
204
|
+
output.write(result)
|
|
205
|
+
return {"saved": str(args.output.resolve()), "bytes": len(result)}
|
|
206
|
+
return result
|
|
207
|
+
return client.request(paths[command], method="POST", data=input, key=key)
|
|
208
|
+
if command == "api":
|
|
209
|
+
try:
|
|
210
|
+
input = json.loads(args.body) if args.body is not None else {} if args.method == "POST" else None
|
|
211
|
+
if input is not None and not isinstance(input, dict):
|
|
212
|
+
raise ValueError()
|
|
213
|
+
except ValueError:
|
|
214
|
+
raise AppError("invalid_json", "--json must be a JSON object") from None
|
|
215
|
+
result = client.request(args.path, method=args.method, data=input, key=key, binary=bool(args.output))
|
|
216
|
+
if args.output:
|
|
217
|
+
with args.output.open("xb") as output:
|
|
218
|
+
output.write(result)
|
|
219
|
+
return {"saved": str(args.output.resolve()), "bytes": len(result)}
|
|
220
|
+
return result
|
|
221
|
+
raise AppError("unknown_command", command)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def main(argv=None):
|
|
225
|
+
args = parser().parse_args(argv)
|
|
226
|
+
try:
|
|
227
|
+
result = run(args)
|
|
228
|
+
if result is not None:
|
|
229
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
230
|
+
return 0
|
|
231
|
+
except AppError as error:
|
|
232
|
+
print(json.dumps(error.as_dict(), ensure_ascii=False), file=sys.stderr)
|
|
233
|
+
return 1
|
|
234
|
+
except OSError as error:
|
|
235
|
+
print(json.dumps({"error": "filesystem_error", "message": str(error)}), file=sys.stderr)
|
|
236
|
+
return 1
|
|
237
|
+
except KeyboardInterrupt:
|
|
238
|
+
return 130
|
wechat_cli/client.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Small synchronous HTTP client used by the CLI and available to Python callers."""
|
|
2
|
+
|
|
3
|
+
import http.client
|
|
4
|
+
import json
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
from urllib.parse import urlencode
|
|
8
|
+
|
|
9
|
+
from .config import api_token
|
|
10
|
+
from .errors import AppError
|
|
11
|
+
from .media import NoRedirect
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApiClient:
|
|
15
|
+
def __init__(self, directory, port=8787, timeout=100):
|
|
16
|
+
self.base = f"http://127.0.0.1:{port}"
|
|
17
|
+
self.token = api_token(directory)
|
|
18
|
+
self.timeout = timeout
|
|
19
|
+
self.opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())
|
|
20
|
+
|
|
21
|
+
def request(self, path, *, method="GET", data=None, key=None, binary=False):
|
|
22
|
+
if not path.startswith("/api/") or "#" in path or "\r" in path or "\n" in path:
|
|
23
|
+
raise AppError("invalid_api_path", "Use a local /api/ path")
|
|
24
|
+
headers = {"Authorization": "Bearer " + self.token}
|
|
25
|
+
if data is not None:
|
|
26
|
+
headers["Content-Type"] = "application/json"
|
|
27
|
+
if key:
|
|
28
|
+
headers["Idempotency-Key"] = key
|
|
29
|
+
request = urllib.request.Request(
|
|
30
|
+
self.base + path,
|
|
31
|
+
data=None if data is None else json.dumps(data).encode(),
|
|
32
|
+
headers=headers,
|
|
33
|
+
method=method,
|
|
34
|
+
)
|
|
35
|
+
try:
|
|
36
|
+
with self.opener.open(request, timeout=self.timeout) as response:
|
|
37
|
+
raw = response.read(128 * 1024**2 + 1)
|
|
38
|
+
if len(raw) > 128 * 1024**2:
|
|
39
|
+
raise AppError("response_too_large", "API response exceeds 128 MiB", 413)
|
|
40
|
+
return raw if binary else json.loads(raw)
|
|
41
|
+
except urllib.error.HTTPError as error:
|
|
42
|
+
try:
|
|
43
|
+
value = json.loads(error.read(65536))
|
|
44
|
+
raise AppError(value["error"], value.get("message", value["error"]), error.code) from None
|
|
45
|
+
except (ValueError, KeyError):
|
|
46
|
+
raise AppError("api_http_error", f"API returned HTTP {error.code}", error.code) from None
|
|
47
|
+
except (urllib.error.URLError, TimeoutError, ConnectionError, http.client.HTTPException):
|
|
48
|
+
raise AppError("api_unavailable", "Local API is unavailable; run wechat-cli start", 503) from None
|
|
49
|
+
|
|
50
|
+
def chats(self):
|
|
51
|
+
return self.request("/api/native/chats")
|
|
52
|
+
|
|
53
|
+
def messages(self, chat_id, limit=50):
|
|
54
|
+
return self.request("/api/native/messages?" + urlencode({"chatId": chat_id, "limit": limit}))
|
|
55
|
+
|
|
56
|
+
def send(self, chat_id, text, *, idempotency_key):
|
|
57
|
+
return self.request(
|
|
58
|
+
"/api/messages/send", method="POST", data={"chatId": chat_id, "text": text}, key=idempotency_key
|
|
59
|
+
)
|
wechat_cli/codec.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Isolated optional SILK codec process (bounded by the parent process timeout)."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import pilk
|
|
6
|
+
|
|
7
|
+
if __name__ == "__main__":
|
|
8
|
+
action, source, destination, rate = sys.argv[1:]
|
|
9
|
+
if action == "decode":
|
|
10
|
+
pilk.decode(source, destination, pcm_rate=int(rate))
|
|
11
|
+
elif action == "encode":
|
|
12
|
+
pilk.encode(source, destination, pcm_rate=int(rate), tencent=True)
|
|
13
|
+
else:
|
|
14
|
+
raise SystemExit(2)
|