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,643 @@
|
|
|
1
|
+
#include <memory>
|
|
2
|
+
#include <new>
|
|
3
|
+
#include <string>
|
|
4
|
+
|
|
5
|
+
static_assert(sizeof(std::shared_ptr<void>) == 16, "Unsupported shared_ptr ABI");
|
|
6
|
+
static_assert(sizeof(std::string) == 24, "Unsupported string ABI");
|
|
7
|
+
|
|
8
|
+
#include <unordered_map>
|
|
9
|
+
#include <vector>
|
|
10
|
+
#include <cstring>
|
|
11
|
+
#include <stdexcept>
|
|
12
|
+
#include <cstdint>
|
|
13
|
+
#include <fstream>
|
|
14
|
+
#include <dlfcn.h>
|
|
15
|
+
#include <filesystem>
|
|
16
|
+
#include <set>
|
|
17
|
+
#include <mutex>
|
|
18
|
+
#include <functional>
|
|
19
|
+
#include <chrono>
|
|
20
|
+
#include <unistd.h>
|
|
21
|
+
#include <sys/stat.h>
|
|
22
|
+
|
|
23
|
+
using Chats = std::unordered_map<std::string, std::pair<std::string,std::string>>;
|
|
24
|
+
static_assert(sizeof(Chats) == 40, "Unsupported hash-map ABI");
|
|
25
|
+
using SharedGetter = std::shared_ptr<void> (*)(void *);
|
|
26
|
+
using ChatGetter = Chats (*)();
|
|
27
|
+
struct NameFunctions { void *services; void *contacts; void *displayName; };
|
|
28
|
+
using NameGetter = std::string (*)(void *,const std::string &,bool);
|
|
29
|
+
static std::shared_ptr<void> contactService(const std::shared_ptr<void> &account,const NameFunctions &api) {
|
|
30
|
+
const auto services=reinterpret_cast<SharedGetter>(api.services)(account.get());
|
|
31
|
+
return services ? reinterpret_cast<SharedGetter>(api.contacts)(services.get()) : nullptr;
|
|
32
|
+
}
|
|
33
|
+
static std::string displayName(const std::shared_ptr<void> &contacts,const NameFunctions &api,const std::string &id) {
|
|
34
|
+
return contacts && !id.empty() ? reinterpret_cast<NameGetter>(api.displayName)(contacts.get(),id,false) : "";
|
|
35
|
+
}
|
|
36
|
+
static thread_local std::string bridgeResult;
|
|
37
|
+
static thread_local unsigned bridgeStage=0;
|
|
38
|
+
extern "C" unsigned bridge_last_stage(){return bridgeStage;}
|
|
39
|
+
|
|
40
|
+
static void quote(std::string &out, const std::string &value, size_t maximum = 65536) {
|
|
41
|
+
if (value.size() > maximum) throw std::runtime_error("native_field_too_large");
|
|
42
|
+
out += '"';
|
|
43
|
+
const char hex[] = "0123456789abcdef";
|
|
44
|
+
for (unsigned char ch : value) {
|
|
45
|
+
if (ch == '"' || ch == '\\') { out += '\\'; out += ch; }
|
|
46
|
+
else if (ch < 32) { out += "\\u00"; out += hex[ch >> 4]; out += hex[ch & 15]; }
|
|
47
|
+
else out += ch;
|
|
48
|
+
}
|
|
49
|
+
out += '"';
|
|
50
|
+
if (out.size() > 16 * 1024 * 1024) throw std::runtime_error("native_result_too_large");
|
|
51
|
+
}
|
|
52
|
+
struct alignas(8) RawRecord {
|
|
53
|
+
unsigned char bytes[0x278];
|
|
54
|
+
~RawRecord() {
|
|
55
|
+
void **vtable; std::memcpy(&vtable,bytes,sizeof(vtable));
|
|
56
|
+
if (vtable) reinterpret_cast<void (*)(void *)>(vtable[0])(this);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
static_assert(sizeof(RawRecord) == 0x278, "Unsupported record stride");
|
|
60
|
+
using Records = std::vector<RawRecord>;
|
|
61
|
+
using MessageGetter = Records (*)(void *,const std::string &,uint32_t,void *);
|
|
62
|
+
static thread_local void *sessionDestroy;
|
|
63
|
+
struct alignas(8) SessionRecord {
|
|
64
|
+
unsigned char bytes[0x218];
|
|
65
|
+
~SessionRecord(){reinterpret_cast<void (*)(void *)>(sessionDestroy)(this);}
|
|
66
|
+
};
|
|
67
|
+
using Sessions=std::unordered_map<std::string,SessionRecord>;
|
|
68
|
+
static_assert(sizeof(Sessions)==40,"Unsupported session-map ABI");
|
|
69
|
+
struct RecentFunctions { void *services; void *messages; void *initial; void *sessionGetter;
|
|
70
|
+
void *sessionMarker; void *copySessions; void *destroySession; void *isFolded; void *isPinned; };
|
|
71
|
+
extern "C" const char *bridge_read_chats_v2(void *enumeration, void *accountGetter, void *manager,void *nameFunctions,void *recentFunctions) {
|
|
72
|
+
try {
|
|
73
|
+
const auto account = reinterpret_cast<SharedGetter>(accountGetter)(manager);
|
|
74
|
+
if (!account) { bridgeResult = "{\"error\":\"native_account_not_ready\"}"; return bridgeResult.c_str(); }
|
|
75
|
+
const auto chats = reinterpret_cast<ChatGetter>(enumeration)();
|
|
76
|
+
const auto *names=static_cast<const NameFunctions *>(nameFunctions);
|
|
77
|
+
const auto contacts=names ? contactService(account,*names) : nullptr;
|
|
78
|
+
const auto *recent=static_cast<const RecentFunctions *>(recentFunctions);
|
|
79
|
+
const auto services=recent ? reinterpret_cast<SharedGetter>(recent->services)(account.get()) : nullptr;
|
|
80
|
+
const auto messages=services ? reinterpret_cast<SharedGetter>(recent->messages)(services.get()) : nullptr;
|
|
81
|
+
if(recent&&!messages)throw std::runtime_error("native_message_service_not_ready");
|
|
82
|
+
// Same service, snapshot copy and record destructor used by native
|
|
83
|
+
// GetAllSessionInf. Read folder membership from the native predicate.
|
|
84
|
+
Sessions sessions;
|
|
85
|
+
std::unordered_map<std::string,bool> foldedSessions,pinnedSessions;
|
|
86
|
+
if(recent){
|
|
87
|
+
void *marker=recent->sessionMarker;
|
|
88
|
+
using ServiceGetter=std::shared_ptr<void> (*)(void *,void *);
|
|
89
|
+
auto **accountVtable=*reinterpret_cast<void ***>(account.get());
|
|
90
|
+
const auto sessionServices=reinterpret_cast<SharedGetter>(accountVtable[0x38/8])(account.get());
|
|
91
|
+
if(!sessionServices)throw std::runtime_error("native_session_service_not_ready");
|
|
92
|
+
const auto sessionService=reinterpret_cast<ServiceGetter>(recent->sessionGetter)(sessionServices.get(),&marker);
|
|
93
|
+
if(!sessionService)throw std::runtime_error("native_session_service_not_ready");
|
|
94
|
+
auto **vtable=*reinterpret_cast<void ***>(sessionService.get());
|
|
95
|
+
const auto *snapshot=reinterpret_cast<const void *(*)(void *)>(vtable[0x78/8])(sessionService.get());
|
|
96
|
+
if(!snapshot)throw std::runtime_error("native_session_service_not_ready");
|
|
97
|
+
sessionDestroy=recent->destroySession;
|
|
98
|
+
reinterpret_cast<void (*)(void *,const void *)>(recent->copySessions)(&sessions,snapshot);
|
|
99
|
+
if(sessions.size()>20000)throw std::runtime_error("native_result_too_large");
|
|
100
|
+
for(const auto &record:sessions){
|
|
101
|
+
const auto &id=record.first;
|
|
102
|
+
if(id.empty()||id.size()>1024)throw std::runtime_error("native_session_layout_unverified");
|
|
103
|
+
foldedSessions.emplace(id,reinterpret_cast<bool (*)(const void *)>(recent->isFolded)(record.second.bytes));
|
|
104
|
+
pinnedSessions.emplace(id,reinterpret_cast<bool (*)(const void *)>(recent->isPinned)(record.second.bytes));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
alignas(8) unsigned char options[80]={};float factor=1;std::memcpy(options+32,&factor,4);
|
|
108
|
+
if (chats.size() > 20000) throw std::runtime_error("native_result_too_large");
|
|
109
|
+
std::string json = "{\"source\":\"native\",\"total\":" + std::to_string(chats.size()) + ",\"items\":[";
|
|
110
|
+
bool first = true;
|
|
111
|
+
for (const auto &chat : chats) {
|
|
112
|
+
if (chat.second.first.empty()) throw std::runtime_error("empty_conversation_id");
|
|
113
|
+
if (!first) json += ','; first = false;
|
|
114
|
+
json += "{\"id\":"; quote(json,chat.second.first,1024);
|
|
115
|
+
json += ",\"displayString\":"; quote(json,chat.second.second);
|
|
116
|
+
json += ",\"displayName\":"; quote(json,names ? displayName(contacts,*names,chat.second.first) : "");
|
|
117
|
+
if(recent) {
|
|
118
|
+
const auto entry=foldedSessions.find(chat.second.first);
|
|
119
|
+
if(entry==foldedSessions.end())throw std::runtime_error("native_session_layout_unverified");
|
|
120
|
+
const bool folded=entry->second;
|
|
121
|
+
json += ",\"isPinned\":"+std::string(pinnedSessions.at(chat.second.first)?"true":"false");
|
|
122
|
+
json += ",\"isMinimized\":"+std::string(folded?"true":"false");
|
|
123
|
+
const auto records=reinterpret_cast<MessageGetter>(recent->initial)(messages.get(),chat.second.first,1,options);
|
|
124
|
+
if(records.size()>1)throw std::runtime_error("native_message_layout_unverified");
|
|
125
|
+
uint32_t latest=0;
|
|
126
|
+
if(!records.empty())std::memcpy(&latest,records.front().bytes+0x114,4);
|
|
127
|
+
json += ",\"lastMessageTime\":"+std::to_string(latest);
|
|
128
|
+
}
|
|
129
|
+
json += ",\"source\":\"native\"}";
|
|
130
|
+
}
|
|
131
|
+
json += "]}"; bridgeResult = std::move(json);
|
|
132
|
+
} catch (const std::runtime_error &error) {
|
|
133
|
+
bridgeResult="{\"error\":";quote(bridgeResult,error.what(),1024);bridgeResult+='}';
|
|
134
|
+
} catch (...) { bridgeResult = "{\"error\":\"native_conversation_layout_unverified\"}"; }
|
|
135
|
+
return bridgeResult.c_str();
|
|
136
|
+
}
|
|
137
|
+
static thread_local void *uiDestroy;
|
|
138
|
+
struct alignas(8) UiRecord {
|
|
139
|
+
unsigned char bytes[0x350];
|
|
140
|
+
~UiRecord(){reinterpret_cast<void (*)(void *)>(uiDestroy)(this);}
|
|
141
|
+
};
|
|
142
|
+
static_assert(sizeof(UiRecord)==0x350,"Unsupported UI record stride");
|
|
143
|
+
using UiRecords=std::vector<UiRecord>;
|
|
144
|
+
static uint64_t uiInteger64(const UiRecord &record,size_t offset);
|
|
145
|
+
static const std::string &uiField(const UiRecord &record,size_t offset);
|
|
146
|
+
struct VoiceTask {std::function<void(const void *)> factory;std::weak_ptr<void> owner;uint64_t token;};
|
|
147
|
+
struct VoiceHandle {uint64_t id;std::shared_ptr<void> resolver;std::shared_ptr<void> owner;};
|
|
148
|
+
struct VoiceLocation {const char *name;const char *file;uint64_t line;void *caller;};
|
|
149
|
+
static_assert(sizeof(VoiceTask)==56&&sizeof(VoiceHandle)==40&&sizeof(VoiceLocation)==32,"Unsupported task ABI");
|
|
150
|
+
struct VoiceJob {std::mutex mutex;std::string text;std::string state="pending";VoiceHandle handle{};std::chrono::steady_clock::time_point created=std::chrono::steady_clock::now();};
|
|
151
|
+
static std::mutex voiceJobsMutex;
|
|
152
|
+
static std::unordered_map<std::string,std::shared_ptr<VoiceJob>> voiceJobs;
|
|
153
|
+
struct TranscribeFunctions {void *initial;void *convert;void *destroy;void *registry;void *descriptor;void *start;void *attach;};
|
|
154
|
+
static std::string voiceJobKey(void *account,const char *chat,const char *id){return std::to_string(reinterpret_cast<uintptr_t>(account))+"|"+chat+"|"+id;}
|
|
155
|
+
static std::string voiceJobJson(const std::shared_ptr<VoiceJob> &job){
|
|
156
|
+
std::lock_guard<std::mutex> guard(job->mutex);
|
|
157
|
+
if(job->state=="pending"&&std::chrono::steady_clock::now()-job->created>std::chrono::seconds(90))job->state="timed_out";
|
|
158
|
+
std::string json="{\"status\":";quote(json,job->state);json+=",\"text\":";if(job->state=="completed")quote(json,job->text);else json+="null";json+="}";return json;
|
|
159
|
+
}
|
|
160
|
+
extern "C" const char *bridge_voice_transcription_result(void *accountGetter,void *manager,const char *chat,const char *id){
|
|
161
|
+
try{const auto account=reinterpret_cast<SharedGetter>(accountGetter)(manager);if(!account)throw std::runtime_error("native_account_not_ready");
|
|
162
|
+
std::lock_guard<std::mutex> guard(voiceJobsMutex);const auto found=voiceJobs.find(voiceJobKey(account.get(),chat,id));
|
|
163
|
+
bridgeResult=found==voiceJobs.end()?"{\"status\":\"unavailable\",\"text\":null}":voiceJobJson(found->second);
|
|
164
|
+
}catch(...){bridgeResult="{\"error\":\"native_account_not_ready\"}";}return bridgeResult.c_str();
|
|
165
|
+
}
|
|
166
|
+
extern "C" const char *bridge_start_voice_transcription(void *accountGetter,void *manager,void *servicesGetter,void *messageServiceGetter,void *functions,const char *chat,const char *id){
|
|
167
|
+
try{
|
|
168
|
+
const auto account=reinterpret_cast<SharedGetter>(accountGetter)(manager);if(!account)throw std::runtime_error("native_account_not_ready");
|
|
169
|
+
const auto services=reinterpret_cast<SharedGetter>(servicesGetter)(account.get());
|
|
170
|
+
const auto service=services?reinterpret_cast<SharedGetter>(messageServiceGetter)(services.get()):nullptr;
|
|
171
|
+
if(!service)throw std::runtime_error("native_message_service_not_ready");
|
|
172
|
+
const auto &api=*static_cast<const TranscribeFunctions *>(functions);
|
|
173
|
+
const auto key=voiceJobKey(account.get(),chat,id);
|
|
174
|
+
std::unique_lock<std::mutex> jobsGuard(voiceJobsMutex);
|
|
175
|
+
for(auto it=voiceJobs.begin();it!=voiceJobs.end();){if(std::chrono::steady_clock::now()-it->second->created>std::chrono::minutes(5))it=voiceJobs.erase(it);else ++it;}
|
|
176
|
+
const auto old=voiceJobs.find(key);if(old!=voiceJobs.end()){bridgeResult=voiceJobJson(old->second);return bridgeResult.c_str();}
|
|
177
|
+
size_t pending=0;for(const auto &entry:voiceJobs){std::lock_guard<std::mutex> guard(entry.second->mutex);pending+=entry.second->state=="pending";}
|
|
178
|
+
if(pending>=2||voiceJobs.size()>=32){bridgeResult="{\"error\":\"native_bridge_busy\"}";return bridgeResult.c_str();}
|
|
179
|
+
const std::string chatId(chat);alignas(8) unsigned char options[80]={};float factor=1;std::memcpy(options+32,&factor,4);options[0x4c]=1;
|
|
180
|
+
const auto raw=reinterpret_cast<MessageGetter>(api.initial)(service.get(),chatId,100,options);
|
|
181
|
+
uiDestroy=api.destroy;const auto records=reinterpret_cast<UiRecords (*)(const std::string &,const Records &)>(api.convert)(chatId,raw);
|
|
182
|
+
const UiRecord *selected=nullptr;for(const auto &record:records)if(std::to_string(uiInteger64(record,0x90))==id){selected=&record;break;}
|
|
183
|
+
if(!selected){bridgeResult="{\"error\":\"native_message_not_found\"}";return bridgeResult.c_str();}
|
|
184
|
+
uint32_t type;std::memcpy(&type,selected->bytes+8,4);if(type!=34){bridgeResult="{\"error\":\"invalid_media_type\"}";return bridgeResult.c_str();}
|
|
185
|
+
const auto job=std::make_shared<VoiceJob>();
|
|
186
|
+
const auto &cached=uiField(*selected,0x108);if(!cached.empty()){job->state="completed";job->text=cached;voiceJobs.emplace(key,job);bridgeResult=voiceJobJson(job);return bridgeResult.c_str();}
|
|
187
|
+
auto **vtable=*reinterpret_cast<void ***>(account.get());
|
|
188
|
+
const auto frontServices=reinterpret_cast<SharedGetter>(vtable[0x30/8])(account.get());
|
|
189
|
+
void *descriptor=api.descriptor;
|
|
190
|
+
using Registry=std::shared_ptr<void> (*)(void *,void *);
|
|
191
|
+
const auto front=frontServices?reinterpret_cast<Registry>(api.registry)(frontServices.get(),&descriptor):nullptr;
|
|
192
|
+
if(!front){bridgeResult="{\"error\":\"native_services_not_ready\"}";return bridgeResult.c_str();}
|
|
193
|
+
using Start=VoiceTask (*)(void *,const void *);
|
|
194
|
+
auto task=reinterpret_cast<Start>(api.start)(front.get(),selected->bytes);
|
|
195
|
+
std::weak_ptr<VoiceJob> weak=job;
|
|
196
|
+
std::function<void(const std::string &)> success=[weak](const std::string &text){if(auto value=weak.lock()){std::lock_guard<std::mutex> guard(value->mutex);if(value->state=="pending"){value->text=text;value->state=text.empty()?"failed":"completed";}}};
|
|
197
|
+
std::function<void(const void *)> failure=[weak](const void *){if(auto value=weak.lock()){std::lock_guard<std::mutex> guard(value->mutex);if(value->state=="pending")value->state="failed";}};
|
|
198
|
+
std::function<void()> finished;
|
|
199
|
+
const VoiceLocation location{"TranscribeVoice","native-api",1,nullptr};
|
|
200
|
+
using Attach=VoiceHandle (*)(VoiceTask *,void *,void *,void *,const VoiceLocation *);
|
|
201
|
+
voiceJobs.emplace(key,job);jobsGuard.unlock();
|
|
202
|
+
job->handle=reinterpret_cast<Attach>(api.attach)(&task,&success,&failure,&finished,&location);
|
|
203
|
+
bridgeResult=voiceJobJson(job);
|
|
204
|
+
}catch(...){bridgeResult="{\"error\":\"native_transcription_failed\"}";}return bridgeResult.c_str();
|
|
205
|
+
}
|
|
206
|
+
static std::mutex mediaPathMutex;
|
|
207
|
+
static std::unordered_map<std::string,std::filesystem::path> nativeMediaPaths;
|
|
208
|
+
static std::unordered_map<std::string,std::string> nativeMediaBlobs;
|
|
209
|
+
static_assert(sizeof(std::filesystem::path)==24,"Unsupported native path ABI");
|
|
210
|
+
struct OcrFunctions {void *getter;void *initialize;void *recognize;};
|
|
211
|
+
struct OcrResult {std::shared_ptr<void> text;std::shared_ptr<void> qr;std::shared_ptr<void> extra;};
|
|
212
|
+
static_assert(sizeof(OcrResult)==48,"Unsupported OCR result ABI");
|
|
213
|
+
static std::mutex ocrMutex;
|
|
214
|
+
struct OcrTemporaryFile {
|
|
215
|
+
std::string path;
|
|
216
|
+
~OcrTemporaryFile(){if(!path.empty())::unlink(path.c_str());}
|
|
217
|
+
};
|
|
218
|
+
extern "C" const char *bridge_extract_image_text(void *accountGetter,void *manager,void *servicesGetter,void *functions,const char *encoded){
|
|
219
|
+
try{
|
|
220
|
+
std::unique_lock<std::mutex> guard(ocrMutex,std::try_to_lock);if(!guard.owns_lock())throw std::runtime_error("native_bridge_busy");
|
|
221
|
+
const size_t inputSize=std::strlen(encoded);if(inputSize==0||inputSize>11184812||inputSize%4)throw std::runtime_error("native_invalid_ocr_image");
|
|
222
|
+
std::string bytes;bytes.reserve(inputSize/4*3);unsigned int accumulator=0,bits=0;bool padding=false;
|
|
223
|
+
for(size_t i=0;i<inputSize;i++){
|
|
224
|
+
const unsigned char c=encoded[i];if(c=='='){padding=true;continue;}if(padding)throw std::runtime_error("native_invalid_ocr_image");
|
|
225
|
+
int value=c>='A'&&c<='Z'?c-'A':c>='a'&&c<='z'?c-'a'+26:c>='0'&&c<='9'?c-'0'+52:c=='+'?62:c=='/'?63:-1;
|
|
226
|
+
if(value<0)throw std::runtime_error("native_invalid_ocr_image");accumulator=(accumulator<<6)|static_cast<unsigned int>(value);bits+=6;
|
|
227
|
+
if(bits>=8){bits-=8;bytes+=static_cast<char>((accumulator>>bits)&255);}
|
|
228
|
+
}
|
|
229
|
+
if(bytes.empty()||bytes.size()>8*1024*1024)throw std::runtime_error("native_invalid_ocr_image");
|
|
230
|
+
const auto account=reinterpret_cast<SharedGetter>(accountGetter)(manager);if(!account)throw std::runtime_error("native_account_not_ready");
|
|
231
|
+
const auto services=reinterpret_cast<SharedGetter>(servicesGetter)(account.get());
|
|
232
|
+
const auto &api=*static_cast<const OcrFunctions *>(functions);
|
|
233
|
+
const auto ocr=services?reinterpret_cast<SharedGetter>(api.getter)(services.get()):nullptr;
|
|
234
|
+
if(!ocr)throw std::runtime_error("native_services_not_ready");
|
|
235
|
+
uint32_t state;std::memcpy(&state,static_cast<const unsigned char *>(ocr.get())+0x20,4);
|
|
236
|
+
if(state!=1){reinterpret_cast<void (*)(void *)>(api.initialize)(ocr.get());std::memcpy(&state,static_cast<const unsigned char *>(ocr.get())+0x20,4);}
|
|
237
|
+
if(state!=1)throw std::runtime_error("native_ocr_not_ready");
|
|
238
|
+
OcrTemporaryFile temporary;
|
|
239
|
+
const auto pattern=(std::filesystem::temp_directory_path()/"wechat-api-ocr-XXXXXX").string();
|
|
240
|
+
std::vector<char> filename(pattern.begin(),pattern.end());filename.push_back(0);
|
|
241
|
+
int fd=::mkstemp(filename.data());if(fd<0)throw std::runtime_error("native_ocr_failed");temporary.path=filename.data();
|
|
242
|
+
size_t written=0;while(written<bytes.size()){const auto n=::write(fd,bytes.data()+written,bytes.size()-written);if(n<=0){::close(fd);throw std::runtime_error("native_ocr_failed");}written+=static_cast<size_t>(n);}::close(fd);
|
|
243
|
+
alignas(8) unsigned char options[32]={};options[0]=1;options[1]=1;uint32_t type=1;std::memcpy(options+8,&type,4);
|
|
244
|
+
const std::filesystem::path path(temporary.path);
|
|
245
|
+
// Direct local IPC entry: deliberately bypasses DoOCRQR's network fallback.
|
|
246
|
+
using Recognize=OcrResult (*)(void *,const std::filesystem::path &,const void *);
|
|
247
|
+
const auto result=reinterpret_cast<Recognize>(api.recognize)(ocr.get(),path,options);
|
|
248
|
+
if(!result.text)throw std::runtime_error("native_ocr_failed");
|
|
249
|
+
const auto *value=static_cast<const unsigned char *>(result.text.get());
|
|
250
|
+
uintptr_t begin,end;std::memcpy(&begin,value+8,8);std::memcpy(&end,value+0x10,8);
|
|
251
|
+
if(end<begin||(end-begin)%0x80||(end-begin)/0x80>10000||(end!=begin&&begin==0))throw std::runtime_error("native_ocr_layout_unverified");
|
|
252
|
+
std::string lines="[",text;size_t total=0;bool first=true;
|
|
253
|
+
for(uintptr_t line=begin;line<end;line+=0x80){
|
|
254
|
+
const auto &part=*reinterpret_cast<const std::string *>(line+0x18);if(part.size()>65536||total+part.size()>1048576)throw std::runtime_error("native_ocr_layout_unverified");total+=part.size();
|
|
255
|
+
if(!first){lines+=',';text+='\n';}first=false;quote(lines,part,65536);text+=part;
|
|
256
|
+
}lines+=']';
|
|
257
|
+
const auto &combined=*reinterpret_cast<const std::string *>(value+0x30);if(!combined.empty()){if(combined.size()>1048576)throw std::runtime_error("native_ocr_layout_unverified");text=combined;}
|
|
258
|
+
bridgeResult="{\"text\":";quote(bridgeResult,text,1048576);bridgeResult+=",\"lines\":"+lines+",\"recognition\":\"wechat\",\"processing\":\"local\"}";
|
|
259
|
+
}catch(const std::runtime_error &error){bridgeResult="{\"error\":";quote(bridgeResult,error.what());bridgeResult+='}';}
|
|
260
|
+
catch(...){bridgeResult="{\"error\":\"native_ocr_failed\"}";}
|
|
261
|
+
return bridgeResult.c_str();
|
|
262
|
+
}
|
|
263
|
+
struct MessageFunctions {void *initial;void *convert;void *destroy;NameFunctions names;void *voicePath;void *voiceData;};
|
|
264
|
+
static uint64_t uiInteger64(const UiRecord &record,size_t offset){uint64_t value;std::memcpy(&value,record.bytes+offset,8);return value;}
|
|
265
|
+
static const std::string &uiField(const UiRecord &record,size_t offset){return *reinterpret_cast<const std::string *>(record.bytes+offset);}
|
|
266
|
+
extern "C" const char *bridge_read_messages(void *accountGetter,void *manager,void *servicesGetter,
|
|
267
|
+
void *messageServiceGetter,void *messageFunctions,const char *chatId,uint32_t limit,const char *mediaMessageId) {
|
|
268
|
+
try {
|
|
269
|
+
bridgeStage=1;const auto account=reinterpret_cast<SharedGetter>(accountGetter)(manager);
|
|
270
|
+
if(!account) {bridgeResult="{\"error\":\"native_account_not_ready\"}";return bridgeResult.c_str();}
|
|
271
|
+
bridgeStage=2;const auto services=reinterpret_cast<SharedGetter>(servicesGetter)(account.get());
|
|
272
|
+
if(!services) {bridgeResult="{\"error\":\"native_services_not_ready\"}";return bridgeResult.c_str();}
|
|
273
|
+
bridgeStage=3;const auto service=reinterpret_cast<SharedGetter>(messageServiceGetter)(services.get());
|
|
274
|
+
if(!service){bridgeResult="{\"error\":\"native_message_service_not_ready\"}";return bridgeResult.c_str();}
|
|
275
|
+
std::string id(chatId);alignas(8) unsigned char options[80]={};float factor=1;std::memcpy(options+32,&factor,4);
|
|
276
|
+
// Installed build's history backend calls RawMsgInfo::ClearContent
|
|
277
|
+
// unless this request flag is set (verified in the installed history backend).
|
|
278
|
+
options[0x4c]=1;
|
|
279
|
+
const auto &api=*static_cast<const MessageFunctions *>(messageFunctions);
|
|
280
|
+
const auto contacts=contactService(account,api.names);
|
|
281
|
+
std::unordered_map<std::string,std::string> nameCache;
|
|
282
|
+
const auto nameFor=[&](const std::string &sender) -> const std::string & {
|
|
283
|
+
auto entry=nameCache.find(sender);
|
|
284
|
+
if(entry==nameCache.end()) entry=nameCache.emplace(sender,displayName(contacts,api.names,sender)).first;
|
|
285
|
+
return entry->second;
|
|
286
|
+
};
|
|
287
|
+
bridgeStage=4;const auto rawRecords=reinterpret_cast<MessageGetter>(api.initial)(service.get(),id,limit,options);
|
|
288
|
+
using Convert=UiRecords (*)(const std::string &,const Records &);
|
|
289
|
+
uiDestroy=api.destroy;
|
|
290
|
+
bridgeStage=5;const auto records=reinterpret_cast<Convert>(api.convert)(id,rawRecords);
|
|
291
|
+
if(records.size()>limit) throw std::runtime_error("native_result_too_large");
|
|
292
|
+
std::string json="{\"source\":\"native\",\"chatId\":";quote(json,id,1024);
|
|
293
|
+
json+=",\"total\":"+std::to_string(records.size())+",\"limit\":"+std::to_string(limit)+",\"items\":[";
|
|
294
|
+
std::unordered_map<uint64_t,const RawRecord *> rawById;
|
|
295
|
+
for(const auto &raw:rawRecords){uint64_t key;std::memcpy(&key,raw.bytes+0xf8,8);rawById.emplace(key,&raw);}
|
|
296
|
+
bool first=true;
|
|
297
|
+
bridgeStage=7;for(const auto &record:records) {
|
|
298
|
+
if(!first)json+=',';first=false;
|
|
299
|
+
uint32_t type;std::memcpy(&type,record.bytes+8,4);
|
|
300
|
+
json+="{\"id\":";quote(json,std::to_string(uiInteger64(record,0x90)));
|
|
301
|
+
json+=",\"serverId\":";quote(json,std::to_string(uiInteger64(record,0xa0)));
|
|
302
|
+
json+=",\"type\":"+std::to_string(type)+",\"from\":";quote(json,uiField(record,0x10),1024);
|
|
303
|
+
json+=",\"to\":";quote(json,uiField(record,0x28),1024);
|
|
304
|
+
json+=",\"groupSender\":";quote(json,uiField(record,0x40),1024);
|
|
305
|
+
const auto &groupSender=uiField(record,0x40);
|
|
306
|
+
json+=",\"senderName\":";quote(json,nameFor(groupSender.empty()?uiField(record,0x10):groupSender));
|
|
307
|
+
uint32_t timestamp;std::memcpy(×tamp,record.bytes+0xa8,4);
|
|
308
|
+
json+=",\"timestamp\":"+std::to_string(timestamp);
|
|
309
|
+
const auto raw=rawById.find(uiInteger64(record,0x90));
|
|
310
|
+
const auto &displayContent=uiField(record,0xb8);
|
|
311
|
+
const auto &originalContent=raw!=rawById.end()?*reinterpret_cast<const std::string *>(raw->second->bytes+0x130):displayContent;
|
|
312
|
+
json+=",\"content\":";quote(json,displayContent.empty()?originalContent:displayContent,1048576);
|
|
313
|
+
if(type==3||type==34||type==43||type==47||type==49||type==62){
|
|
314
|
+
json+=",\"nativeMediaFields\": {";bool firstField=true;
|
|
315
|
+
if(uiInteger64(record,0xa0)!=0){
|
|
316
|
+
using Md5=unsigned char *(*)(const void *,uint32_t,unsigned char *);
|
|
317
|
+
auto md5=reinterpret_cast<Md5>(dlsym(RTLD_DEFAULT,"CC_MD5"));
|
|
318
|
+
if(md5){const auto server=std::to_string(uiInteger64(record,0xa0));unsigned char digest[16];md5(server.data(),server.size(),digest);std::string hash;const char digits[]="0123456789abcdef";for(unsigned char c:digest){hash+=digits[c>>4];hash+=digits[c&15];}
|
|
319
|
+
quote(json,"serverHash");json+=':';quote(json,hash);json+=',';quote(json,"serverHashMiddle");json+=':';quote(json,hash.substr(8,16));firstField=false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if(type==34&&mediaMessageId&&std::to_string(uiInteger64(record,0x90))==mediaMessageId&&raw!=rawById.end()){
|
|
323
|
+
const auto key="native_voice_"+std::to_string(uiInteger64(record,0x90))+"_"+std::to_string(uiInteger64(record,0xa0));bool available=false;
|
|
324
|
+
using VoiceGetter=std::shared_ptr<void> (*)(void *,const void *,const std::string &);
|
|
325
|
+
const std::string index="0";
|
|
326
|
+
const auto voice=reinterpret_cast<VoiceGetter>(api.voiceData)(service.get(),raw->second->bytes,index);
|
|
327
|
+
if(voice){
|
|
328
|
+
const auto &buffer=*reinterpret_cast<const std::shared_ptr<std::vector<unsigned char>> *>(static_cast<const unsigned char *>(voice.get())+0x60);
|
|
329
|
+
if(buffer&&!buffer->empty()&&buffer->size()<=1024*1024){
|
|
330
|
+
const std::string bytes(reinterpret_cast<const char *>(buffer->data()),buffer->size());
|
|
331
|
+
std::lock_guard<std::mutex> guard(mediaPathMutex);if(nativeMediaBlobs.size()>=32)nativeMediaBlobs.clear();nativeMediaBlobs[key]=bytes;available=true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if(!available&&api.voicePath){
|
|
335
|
+
// REA/ARM64: hidden x8 path result, x0 MsgId (raw+0xf8),
|
|
336
|
+
// x1 RawMsgInfo, w2 format (4=SILK).
|
|
337
|
+
using PathGetter=std::filesystem::path (*)(const void *,const void *,uint32_t);
|
|
338
|
+
const auto path=reinterpret_cast<PathGetter>(api.voicePath)(raw->second->bytes+0xf8,raw->second->bytes,4);
|
|
339
|
+
std::error_code error;if(path.is_absolute()&&std::filesystem::is_regular_file(path,error)){
|
|
340
|
+
std::lock_guard<std::mutex> guard(mediaPathMutex);if(nativeMediaPaths.size()>1000)nativeMediaPaths.clear();nativeMediaPaths[key]=path;available=true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if(available){if(!firstField)json+=',';quote(json,"voiceFile");json+=':';quote(json,key);firstField=false;}
|
|
344
|
+
}
|
|
345
|
+
for(const size_t offset:{0x78,0xd0,0x108,0x120,0x140,0x158,0x170,0x188,0x1a0,0x1b8,0x1e0,0x1f8}){
|
|
346
|
+
const auto &field=uiField(record,offset);if(field.empty())continue;
|
|
347
|
+
if(!firstField)json+=',';firstField=false;
|
|
348
|
+
quote(json,std::to_string(offset));json+=':';quote(json,field);
|
|
349
|
+
}json+='}';
|
|
350
|
+
}
|
|
351
|
+
json+=",\"source\":\"native\",\"chatId\":";quote(json,id,1024);json+='}';
|
|
352
|
+
}
|
|
353
|
+
bridgeStage=8;json+="]}";bridgeResult=std::move(json);
|
|
354
|
+
} catch(...) {bridgeResult="{\"error\":\"native_message_layout_unverified\"}";}
|
|
355
|
+
return bridgeResult.c_str();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
extern "C" bool bridge_has_task_context(void *descriptor) {
|
|
359
|
+
auto resolver=*static_cast<void *(**)(void *)>(descriptor);
|
|
360
|
+
void *slot=resolver(descriptor);
|
|
361
|
+
return slot && *static_cast<void **>(slot)!=nullptr;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
struct SendFunctions { void *uuid; void *construct; void *send; void *destroyResult; };
|
|
365
|
+
struct alignas(8) SendResult {unsigned char bytes[0x40];};
|
|
366
|
+
extern "C" const char *bridge_probe_text_processor_v2(void *accountGetter,void *manager,
|
|
367
|
+
void *servicesGetter,void *messageServiceGetter,void *processorGetter) {
|
|
368
|
+
try {
|
|
369
|
+
const auto account=reinterpret_cast<SharedGetter>(accountGetter)(manager);
|
|
370
|
+
const auto services=account?reinterpret_cast<SharedGetter>(servicesGetter)(account.get()):nullptr;
|
|
371
|
+
const auto service=services?reinterpret_cast<SharedGetter>(messageServiceGetter)(services.get()):nullptr;
|
|
372
|
+
if(!service)throw std::runtime_error("native_message_service_not_ready");
|
|
373
|
+
const uint32_t type=1,subtype=0;
|
|
374
|
+
using Getter=std::shared_ptr<void> (*)(void *,const uint32_t *,const uint32_t *);
|
|
375
|
+
const auto processor=reinterpret_cast<Getter>(processorGetter)(service.get(),&type,&subtype);
|
|
376
|
+
bridgeResult="{\"source\":\"native\",\"textProcessorAvailable\":"+std::string(processor?"true":"false")+",\"processorVtable\":\""+std::to_string(processor?reinterpret_cast<uintptr_t>(*reinterpret_cast<void **>(processor.get())):0)+"\"}";
|
|
377
|
+
}catch(...){bridgeResult="{\"error\":\"native_processor_probe_failed\"}";}
|
|
378
|
+
return bridgeResult.c_str();
|
|
379
|
+
}
|
|
380
|
+
static const char *bridge_text_v2(void *accountGetter,void *manager,void *servicesGetter,
|
|
381
|
+
void *messageServiceGetter,void *functions,const char *chatId,const char *content,bool prepare) {
|
|
382
|
+
try {
|
|
383
|
+
// The caller validates this recipient against the active conversation list.
|
|
384
|
+
const std::string id(chatId),text(content);
|
|
385
|
+
|
|
386
|
+
const auto &api=*static_cast<const SendFunctions *>(functions);
|
|
387
|
+
const auto account=reinterpret_cast<SharedGetter>(accountGetter)(manager);
|
|
388
|
+
if(!account)throw std::runtime_error("native_account_not_ready");
|
|
389
|
+
const auto services=reinterpret_cast<SharedGetter>(servicesGetter)(account.get());
|
|
390
|
+
const auto service=services?reinterpret_cast<SharedGetter>(messageServiceGetter)(services.get()):nullptr;
|
|
391
|
+
if(!service)throw std::runtime_error("native_message_service_not_ready");
|
|
392
|
+
const auto uuid=reinterpret_cast<std::string (*)()>(api.uuid)();
|
|
393
|
+
// Native UiSendInfo factory owns the embedded 0x350-byte UiRecord.
|
|
394
|
+
// The send processor requires this type; RawSendInfo fails its cast.
|
|
395
|
+
const auto owned=reinterpret_cast<std::shared_ptr<void> (*)()>(api.construct)();
|
|
396
|
+
if(!owned)throw std::runtime_error("native_send_info_empty");
|
|
397
|
+
auto *info=static_cast<unsigned char *>(owned.get());
|
|
398
|
+
auto *record=info+0xe8;
|
|
399
|
+
auto **managerVtable=*reinterpret_cast<void ***>(manager);
|
|
400
|
+
const auto &self=reinterpret_cast<const std::string &(*)(void *)>(managerVtable[0x28/8])(manager);
|
|
401
|
+
if(self.empty())throw std::runtime_error("native_account_not_ready");
|
|
402
|
+
*reinterpret_cast<std::string *>(info+0x90)=id;
|
|
403
|
+
uint32_t type=1;std::memcpy(record+8,&type,4);
|
|
404
|
+
*reinterpret_cast<std::string *>(record+0x10)=self;
|
|
405
|
+
*reinterpret_cast<std::string *>(record+0x28)=id;
|
|
406
|
+
*reinterpret_cast<std::string *>(record+0xb8)=text;
|
|
407
|
+
const bool receiverVerified=*reinterpret_cast<const std::string *>(info+0x90)==id&&*reinterpret_cast<const std::string *>(record+0x28)==id;
|
|
408
|
+
if(prepare){
|
|
409
|
+
bridgeResult="{\"source\":\"native\",\"prepared\":true,\"queued\":false,\"receiverVerified\":"+std::string(receiverVerified?"true":"false")+",\"requestId\":";
|
|
410
|
+
quote(bridgeResult,uuid);bridgeResult+="}";return bridgeResult.c_str();
|
|
411
|
+
}
|
|
412
|
+
const std::vector<std::shared_ptr<void>> requests{owned};
|
|
413
|
+
using Send=SendResult (*)(void *,const std::vector<std::shared_ptr<void>> &);
|
|
414
|
+
auto result=reinterpret_cast<Send>(api.send)(service.get(),requests);
|
|
415
|
+
const bool queued=result.bytes[0]==1;
|
|
416
|
+
const std::string reason=*reinterpret_cast<const std::string *>(result.bytes+8);
|
|
417
|
+
// Both success and error paths own native result strings/vectors.
|
|
418
|
+
reinterpret_cast<void (*)(void *)>(api.destroyResult)(&result);
|
|
419
|
+
bridgeResult="{\"source\":\"native\",\"queued\":"+std::string(queued?"true":"false")+",\"requestId\":";
|
|
420
|
+
quote(bridgeResult,uuid);bridgeResult+=",\"requestKind\":\"ui\",\"receiverVerified\":"+std::string(receiverVerified?"true":"false");bridgeResult+=",\"nativeError\":";quote(bridgeResult,reason,4096);bridgeResult+="}";
|
|
421
|
+
}catch(...) {bridgeResult="{\"error\":\"native_send_failed\"}";}
|
|
422
|
+
return bridgeResult.c_str();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
extern "C" const char *bridge_send_text_v2(void *a,void *b,void *c,void *d,void *e,const char *f,const char *g){return bridge_text_v2(a,b,c,d,e,f,g,false);}
|
|
426
|
+
extern "C" const char *bridge_prepare_text_v2(void *a,void *b,void *c,void *d,void *e,const char *f,const char *g){return bridge_text_v2(a,b,c,d,e,f,g,true);}
|
|
427
|
+
|
|
428
|
+
struct VoiceSendOptions {uint64_t first=0;uint64_t second=0;uint8_t flag=0;uint8_t padding[7]={};std::string extra;};
|
|
429
|
+
static_assert(sizeof(VoiceSendOptions)==48,"Unsupported voice send options ABI");
|
|
430
|
+
extern "C" const char *bridge_send_voice(void *accountGetter,void *manager,void *servicesGetter,
|
|
431
|
+
void *messageServiceGetter,void *functions,const char *chatId,const void *data,uint32_t size,uint32_t duration,bool prepare) {
|
|
432
|
+
bool submitted=false;
|
|
433
|
+
try {
|
|
434
|
+
if(!data||size<10||size>1024*1024||duration<1000||duration>60000)throw std::runtime_error("invalid_voice");
|
|
435
|
+
const std::string bytes(static_cast<const char *>(data),size),id(chatId);
|
|
436
|
+
if(bytes.compare(0,10,"\x02#!SILK_V3")!=0&&bytes.compare(0,9,"#!SILK_V3")!=0)throw std::runtime_error("invalid_voice");
|
|
437
|
+
const auto &api=*static_cast<const SendFunctions *>(functions);
|
|
438
|
+
const auto account=reinterpret_cast<SharedGetter>(accountGetter)(manager);
|
|
439
|
+
const auto services=account?reinterpret_cast<SharedGetter>(servicesGetter)(account.get()):nullptr;
|
|
440
|
+
const auto service=services?reinterpret_cast<SharedGetter>(messageServiceGetter)(services.get()):nullptr;
|
|
441
|
+
if(!service)throw std::runtime_error("native_message_service_not_ready");
|
|
442
|
+
const VoiceSendOptions options{};
|
|
443
|
+
// UI SendVoice constructs this native subclass with recipient, encoded
|
|
444
|
+
// audio, duration and options. x0 is unused; shared_ptr result is x8.
|
|
445
|
+
using Factory=std::shared_ptr<void> (*)(void *,const std::string &,const std::string &,const uint32_t *,const VoiceSendOptions &);
|
|
446
|
+
const auto owned=reinterpret_cast<Factory>(api.construct)(nullptr,id,bytes,&duration,options);
|
|
447
|
+
if(!owned)throw std::runtime_error("native_send_info_empty");
|
|
448
|
+
auto *info=static_cast<unsigned char *>(owned.get());
|
|
449
|
+
const auto &uuid=*reinterpret_cast<const std::string *>(info+0x580);
|
|
450
|
+
if(uuid.empty()||uuid.size()>128)throw std::runtime_error("native_voice_layout_unverified");
|
|
451
|
+
uint32_t type=34;std::memcpy(info+0xe4,&type,4);
|
|
452
|
+
const bool verified=*reinterpret_cast<const std::string *>(info+0x90)==id&&*reinterpret_cast<const std::string *>(info+0x5c8)==bytes;
|
|
453
|
+
if(!verified)throw std::runtime_error("native_voice_layout_unverified");
|
|
454
|
+
bool queued=false;
|
|
455
|
+
if(!prepare){
|
|
456
|
+
const std::vector<std::shared_ptr<void>> requests{owned};
|
|
457
|
+
using Send=SendResult (*)(void *,const std::vector<std::shared_ptr<void>> &);
|
|
458
|
+
submitted=true;auto result=reinterpret_cast<Send>(api.send)(service.get(),requests);
|
|
459
|
+
queued=result.bytes[0]==1;reinterpret_cast<void (*)(void *)>(api.destroyResult)(&result);
|
|
460
|
+
}
|
|
461
|
+
bridgeResult="{\"source\":\"native\",\"queued\":"+std::string(queued?"true":"false")+",\"prepared\":"+std::string(prepare?"true":"false")+",\"receiverVerified\":true,\"requestId\":";
|
|
462
|
+
quote(bridgeResult,uuid);bridgeResult+="}";
|
|
463
|
+
}catch(const std::runtime_error &error){bridgeResult="{\"error\":";quote(bridgeResult,submitted?"native_send_outcome_unknown":error.what());bridgeResult+='}';}
|
|
464
|
+
catch(...){bridgeResult=submitted?"{\"error\":\"native_send_outcome_unknown\"}":"{\"error\":\"native_voice_prepare_failed\"}";}
|
|
465
|
+
return bridgeResult.c_str();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
#include <filesystem>
|
|
469
|
+
#include <algorithm>
|
|
470
|
+
#include <dlfcn.h>
|
|
471
|
+
namespace fs=std::filesystem;
|
|
472
|
+
static std::string nsPath(void *object){
|
|
473
|
+
if(!object)return "";
|
|
474
|
+
auto selector=reinterpret_cast<void *(*)(const char *)>(dlsym(RTLD_DEFAULT,"sel_registerName"));
|
|
475
|
+
auto message=reinterpret_cast<const char *(*)(void *,void *)>(dlsym(RTLD_DEFAULT,"objc_msgSend"));
|
|
476
|
+
const char *path=message(object,selector("UTF8String"));return path?path:"";
|
|
477
|
+
}
|
|
478
|
+
static std::vector<fs::path> mediaRoots(){
|
|
479
|
+
auto home=reinterpret_cast<void *(*)()>(dlsym(RTLD_DEFAULT,"NSHomeDirectory"));
|
|
480
|
+
std::vector<fs::path> roots;if(home){const auto path=nsPath(home());if(!path.empty()&&fs::path(path).is_absolute())roots.push_back(path);}
|
|
481
|
+
auto cls=reinterpret_cast<void *(*)(const char *)>(dlsym(RTLD_DEFAULT,"objc_getClass"));
|
|
482
|
+
auto sel=reinterpret_cast<void *(*)(const char *)>(dlsym(RTLD_DEFAULT,"sel_registerName"));
|
|
483
|
+
auto msg=reinterpret_cast<void *(*)(void *,void *)>(dlsym(RTLD_DEFAULT,"objc_msgSend"));
|
|
484
|
+
auto arg=reinterpret_cast<void *(*)(void *,void *,const void *)>(dlsym(RTLD_DEFAULT,"objc_msgSend"));
|
|
485
|
+
if(cls&&sel&&msg&&arg){
|
|
486
|
+
void *manager=msg(cls("NSFileManager"),sel("defaultManager"));
|
|
487
|
+
void *group=arg(cls("NSString"),sel("stringWithUTF8String:"),"5A4RE8SF68.com.tencent.xinWeChat");
|
|
488
|
+
void *url=arg(manager,sel("containerURLForSecurityApplicationGroupIdentifier:"),group);
|
|
489
|
+
if(url){const auto path=nsPath(msg(url,sel("path")));if(!path.empty())roots.push_back(path);}
|
|
490
|
+
}return roots;
|
|
491
|
+
}
|
|
492
|
+
static std::string base64(const std::string &data){
|
|
493
|
+
static const char table[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
494
|
+
std::string out;out.reserve((data.size()+2)/3*4);
|
|
495
|
+
for(size_t n=0;n<data.size();n+=3){unsigned a=static_cast<unsigned char>(data[n]),b=n+1<data.size()?static_cast<unsigned char>(data[n+1]):0,c=n+2<data.size()?static_cast<unsigned char>(data[n+2]):0;unsigned v=(a<<16)|(b<<8)|c;out+=table[v>>18];out+=table[(v>>12)&63];out+=n+1<data.size()?table[(v>>6)&63]:'=';out+=n+2<data.size()?table[v&63]:'=';}return out;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
static bool imageHeader(const std::string &data){
|
|
499
|
+
return data.size()>=12&&(data.compare(0,3,"\xff\xd8\xff")==0||data.compare(0,4,"\x89PNG")==0||data.compare(0,3,"GIF")==0||(data.compare(0,4,"RIFF")==0&&data.compare(8,4,"WEBP")==0));
|
|
500
|
+
}
|
|
501
|
+
static std::string decodeDat(const std::string &data,const fs::path &path,const std::vector<std::string> &uins){
|
|
502
|
+
if(data.size()<31||data.compare(0,6,"\x07\x08V2\x08\x07")!=0)return data;
|
|
503
|
+
uint32_t aesSize,xorSize;std::memcpy(&aesSize,data.data()+6,4);std::memcpy(&xorSize,data.data()+10,4);
|
|
504
|
+
const size_t aligned=(static_cast<size_t>(aesSize)/16+1)*16;
|
|
505
|
+
if(aligned>data.size()-15||xorSize>data.size()-15-aligned)return data;
|
|
506
|
+
std::string account;bool next=false;for(const auto &component:path){if(next){account=component.string();break;}if(component=="xwechat_files")next=true;}
|
|
507
|
+
std::vector<std::string> accounts{account};const auto suffix=account.rfind('_');if(suffix!=std::string::npos){if(account.compare(0,5,"wxid_")==0&&suffix>4)accounts.push_back(account.substr(0,suffix));else if(account.size()-suffix==5)accounts.push_back(account.substr(0,suffix));}
|
|
508
|
+
using Md5=unsigned char *(*)(const void *,uint32_t,unsigned char *);
|
|
509
|
+
using Crypt=int (*)(unsigned,unsigned,unsigned,const void *,size_t,const void *,const void *,size_t,void *,size_t,size_t *);
|
|
510
|
+
auto md5=reinterpret_cast<Md5>(dlsym(RTLD_DEFAULT,"CC_MD5"));auto crypt=reinterpret_cast<Crypt>(dlsym(RTLD_DEFAULT,"CCCrypt"));if(!md5||!crypt)return data;
|
|
511
|
+
for(const auto &uin:uins)for(const auto &id:accounts){
|
|
512
|
+
if(id.empty())continue;unsigned char digest[16];const auto input=uin+id;md5(input.data(),input.size(),digest);std::string key;const char digits[]="0123456789abcdef";for(unsigned char c:digest){key+=digits[c>>4];key+=digits[c&15];}key.resize(16);
|
|
513
|
+
std::string plain(aligned,'\0');size_t written=0;
|
|
514
|
+
if(crypt(1,0,3,key.data(),key.size(),nullptr,data.data()+15,aligned,plain.data(),plain.size(),&written)!=0)continue;plain.resize(written);
|
|
515
|
+
if(written!=aesSize||!imageHeader(plain))continue;
|
|
516
|
+
plain.append(data,15+aligned,data.size()-xorSize-15-aligned);
|
|
517
|
+
const unsigned char xorKey=static_cast<unsigned char>(std::stoull(uin)&255);
|
|
518
|
+
for(size_t n=data.size()-xorSize;n<data.size();n++)plain+=static_cast<char>(static_cast<unsigned char>(data[n])^xorKey);
|
|
519
|
+
if(plain.compare(0,3,"\xff\xd8\xff")==0&&plain.compare(plain.size()-2,2,"\xff\xd9")!=0)continue;
|
|
520
|
+
return plain;
|
|
521
|
+
}
|
|
522
|
+
return data;
|
|
523
|
+
}
|
|
524
|
+
extern "C" const char *bridge_cached_media(const char *fingerprint){
|
|
525
|
+
try{
|
|
526
|
+
const std::string hash(fingerprint);if(hash.size()<8||hash.size()>128||hash.find("..")!=std::string::npos||hash.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-")!=std::string::npos)throw std::runtime_error("native_invalid_media_id");
|
|
527
|
+
{std::lock_guard<std::mutex> guard(mediaPathMutex);const auto found=nativeMediaBlobs.find(hash);if(found!=nativeMediaBlobs.end()){bridgeResult="{\"files\":[{\"extension\":\".silk\",\"dataBase64\":\""+base64(found->second)+"\"}]}";return bridgeResult.c_str();}}
|
|
528
|
+
size_t visited=0,accessible=0,totalBytes=0;bool tooLarge=false;std::vector<fs::path> candidates;
|
|
529
|
+
{std::lock_guard<std::mutex> guard(mediaPathMutex);const auto found=nativeMediaPaths.find(hash);if(found!=nativeMediaPaths.end())candidates.push_back(found->second);}std::vector<std::string> uins;
|
|
530
|
+
for(const auto &root:candidates.empty()?mediaRoots():std::vector<fs::path>{}){
|
|
531
|
+
std::error_code error;fs::recursive_directory_iterator it(root,fs::directory_options::skip_permission_denied,error),end;
|
|
532
|
+
if(error)continue;accessible++;
|
|
533
|
+
for(;it!=end&&visited<150000;it.increment(error)){
|
|
534
|
+
if(error){error.clear();continue;}visited++;
|
|
535
|
+
if(it->is_symlink(error)){it.disable_recursion_pending();continue;}if(!it->is_regular_file(error))continue;
|
|
536
|
+
const auto path=it->path();const auto name=path.filename().string();
|
|
537
|
+
if(name.compare(0,4,"key_")==0&&path.extension()==".statistic"){
|
|
538
|
+
const auto stop=name.find('_',4);const auto code=name.substr(4,stop-4);
|
|
539
|
+
if(code.size()>0&&code.size()<=10&&code.find_first_not_of("0123456789")==std::string::npos&&uins.size()<32)uins.push_back(code);
|
|
540
|
+
}
|
|
541
|
+
auto candidate=path.string(),needle=hash;if(hash.find_first_not_of("0123456789abcdefABCDEF")==std::string::npos){for(char &c:candidate)if(c>='A'&&c<='Z')c+=32;for(char &c:needle)if(c>='A'&&c<='Z')c+=32;}
|
|
542
|
+
if(candidate.find(needle)!=std::string::npos&&candidates.size()<12)candidates.push_back(path);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// Prefer originals to thumbnails, then permit a thumbnail fallback.
|
|
546
|
+
std::sort(candidates.begin(),candidates.end(),[](const fs::path &a,const fs::path &b){return a.filename().string().find("_t.dat")==std::string::npos&&b.filename().string().find("_t.dat")!=std::string::npos;});
|
|
547
|
+
std::string json="{\"files\":[";bool first=true;
|
|
548
|
+
for(const auto &path:candidates){std::error_code error;const auto size=fs::file_size(path,error);if(error||size==0)continue;if(size>128*1024*1024){tooLarge=true;continue;}if(totalBytes+size>128*1024*1024)continue;
|
|
549
|
+
std::ifstream input(path,std::ios::binary);std::string data(size,'\0');if(!input.read(data.data(),size))continue;data=decodeDat(data,path,uins);totalBytes+=size;
|
|
550
|
+
if(!first)json+=',';first=false;json+="{\"extension\":";quote(json,path.extension().string());json+=",\"isThumbnail\":"+std::string(path.filename().string().find("_t.dat")!=std::string::npos?"true":"false");json+=",\"dataBase64\":\""+base64(data)+"\"}";
|
|
551
|
+
}
|
|
552
|
+
json+="],\"tooLarge\":"+std::string(tooLarge?"true":"false")+",\"visited\":"+std::to_string(visited)+",\"accessibleRoots\":"+std::to_string(accessible)+"}";bridgeResult=std::move(json);
|
|
553
|
+
}catch(...){bridgeResult="{\"error\":\"native_media_read_failed\"}";}return bridgeResult.c_str();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
extern "C" const char *bridge_capture_recorded_voice(const void *context,const void *audio){
|
|
557
|
+
try{
|
|
558
|
+
const auto *value=static_cast<const unsigned char *>(audio);
|
|
559
|
+
uint32_t first,second;std::memcpy(&first,value,4);std::memcpy(&second,value+4,4);
|
|
560
|
+
const uint64_t duration=static_cast<uint64_t>(first)*second;
|
|
561
|
+
const auto &data=*reinterpret_cast<const std::string *>(value+8);
|
|
562
|
+
const auto &chat=*reinterpret_cast<const std::string *>(static_cast<const unsigned char *>(context)+0x1c0);
|
|
563
|
+
if(duration<1000||duration>60000||data.empty()||data.size()>1048576)throw std::runtime_error("native_recording_invalid");
|
|
564
|
+
bridgeResult="{\"durationMs\":"+std::to_string(duration)+",\"chatId\":";quote(bridgeResult,chat,1024);bridgeResult+=",\"dataBase64\":";quote(bridgeResult,base64(data),1398104);bridgeResult+='}';
|
|
565
|
+
}catch(...){bridgeResult="{\"error\":\"native_recording_invalid\"}";}return bridgeResult.c_str();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
#include <dispatch/dispatch.h>
|
|
569
|
+
struct ImportControlWork {void *window;void *stop;void *cancel;void *busy;uint32_t action;uint32_t kind=0;const char *error=nullptr;};
|
|
570
|
+
static void importControlMain(void *context){
|
|
571
|
+
auto &work=*static_cast<ImportControlWork *>(context);
|
|
572
|
+
try{
|
|
573
|
+
work.kind=reinterpret_cast<uint32_t (*)()>(work.busy)();
|
|
574
|
+
if(work.action==1){
|
|
575
|
+
if(work.kind!=0){work.error="native_migration_busy";return;}
|
|
576
|
+
reinterpret_cast<void (*)(uint32_t)>(work.window)(1);
|
|
577
|
+
}else if(work.action==2){
|
|
578
|
+
if(work.kind!=0&&work.kind!=4){work.error="native_migration_busy";return;}
|
|
579
|
+
reinterpret_cast<void (*)()>(work.cancel)();
|
|
580
|
+
reinterpret_cast<void (*)()>(work.stop)();
|
|
581
|
+
// The window factory activates an existing window when passed 0;
|
|
582
|
+
// closing is performed through its exact Accessibility close control.
|
|
583
|
+
}
|
|
584
|
+
work.kind=reinterpret_cast<uint32_t (*)()>(work.busy)();
|
|
585
|
+
}catch(...){work.error="native_migration_failed";}
|
|
586
|
+
}
|
|
587
|
+
extern "C" const char *bridge_import_control(void *window,void *stop,void *cancel,void *busy,uint32_t action){
|
|
588
|
+
ImportControlWork work{window,stop,cancel,busy,action};
|
|
589
|
+
dispatch_sync_f(dispatch_get_main_queue(),&work,importControlMain);
|
|
590
|
+
if(work.error){bridgeResult="{\"error\":";quote(bridgeResult,work.error);bridgeResult+='}';}
|
|
591
|
+
else bridgeResult="{\"source\":\"native\",\"busyKind\":"+std::to_string(work.kind)+"}";
|
|
592
|
+
return bridgeResult.c_str();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
extern "C" const char *bridge_self_id(void *accountGetter,void *manager){
|
|
596
|
+
try{
|
|
597
|
+
const auto account=reinterpret_cast<SharedGetter>(accountGetter)(manager);
|
|
598
|
+
if(!account)throw std::runtime_error("native_account_not_ready");
|
|
599
|
+
auto **vtable=*reinterpret_cast<void ***>(manager);
|
|
600
|
+
const auto &id=reinterpret_cast<const std::string &(*)(void *)>(vtable[0x28/8])(manager);
|
|
601
|
+
if(id.empty())throw std::runtime_error("native_account_not_ready");
|
|
602
|
+
bridgeResult="{\"id\":";quote(bridgeResult,id,1024);bridgeResult+='}';
|
|
603
|
+
}catch(...){bridgeResult="{\"error\":\"native_account_not_ready\"}";}
|
|
604
|
+
return bridgeResult.c_str();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Attachment lookup accepts native record metadata, never a client path.
|
|
608
|
+
extern "C" const char *bridge_cached_attachment(const char *fileName,uint64_t expectedSize,const char *accountId){
|
|
609
|
+
try{
|
|
610
|
+
const std::string name(fileName),account(accountId);
|
|
611
|
+
if(name.empty()||name.size()>512||name=="."||name==".."||name.find_first_of("/\\")!=std::string::npos||account.empty()||!expectedSize||expectedSize>128*1024*1024)throw std::runtime_error("native_invalid_media_id");
|
|
612
|
+
std::vector<fs::path> candidates;size_t visited=0,nameMatches=0,sizeMatches=0,ownedMatches=0;std::set<std::string> seen;
|
|
613
|
+
for(const auto &root:mediaRoots()){
|
|
614
|
+
std::error_code error;fs::recursive_directory_iterator it(root,fs::directory_options::skip_permission_denied,error),end;
|
|
615
|
+
for(;it!=end&&visited<500000;it.increment(error)){
|
|
616
|
+
if(error){error.clear();continue;}visited++;
|
|
617
|
+
if(it->is_symlink(error)){it.disable_recursion_pending();continue;}
|
|
618
|
+
if(it->is_directory(error)){
|
|
619
|
+
const auto directory=it->path().filename().string();
|
|
620
|
+
if(directory==".git"||directory=="node_modules"||directory==".codex"||directory==".tools")it.disable_recursion_pending();
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if(!it->is_regular_file(error))continue;
|
|
624
|
+
const bool named=it->path().filename().string()==name,sized=fs::file_size(it->path(),error)==expectedSize;
|
|
625
|
+
if(named)nameMatches++;if(sized)sizeMatches++;
|
|
626
|
+
if(!named||!sized)continue;
|
|
627
|
+
bool next=false,owned=false,inCache=false;
|
|
628
|
+
for(const auto &part:it->path()){
|
|
629
|
+
const auto component=part.string();
|
|
630
|
+
if(next){owned=component==account||component.compare(0,account.size()+1,account+"_")==0;next=false;}
|
|
631
|
+
if(component=="xwechat_files"){next=true;inCache=true;}
|
|
632
|
+
}
|
|
633
|
+
if(!inCache||!owned)continue;ownedMatches++;
|
|
634
|
+
const auto path=fs::canonical(it->path(),error);if(!error&&seen.insert(path.string()).second)candidates.push_back(path);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if(candidates.size()>1){bridgeResult="{\"error\":\"native_attachment_ambiguous\"}";return bridgeResult.c_str();}
|
|
638
|
+
std::string json="{\"files\":[";
|
|
639
|
+
if(candidates.size()==1){std::ifstream input(candidates.front(),std::ios::binary);std::string data(expectedSize,'\0');if(!input.read(data.data(),expectedSize))throw std::runtime_error("native_media_read_failed");json+="{\"dataBase64\":\""+base64(data)+"\"}";}
|
|
640
|
+
bridgeResult=json+"],\"cacheLookup\":{\"nameMatches\":"+std::to_string(nameMatches)+",\"sizeMatches\":"+std::to_string(sizeMatches)+",\"ownedMatches\":"+std::to_string(ownedMatches)+",\"visited\":"+std::to_string(visited)+"}}";
|
|
641
|
+
}catch(const std::runtime_error &error){bridgeResult="{\"error\":";quote(bridgeResult,error.what());bridgeResult+='}';}
|
|
642
|
+
catch(...){bridgeResult="{\"error\":\"native_media_read_failed\"}";}return bridgeResult.c_str();
|
|
643
|
+
}
|