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 ADDED
@@ -0,0 +1,3 @@
1
+ """Python API and command line interface for a local macOS WeChat session."""
2
+
3
+ __version__ = "0.1.0"
wechat_cli/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+ const el = id => document.getElementById(id);
3
+ let selected = '', chats = [], busy = false, sequence = 0;
4
+ const attempts = new Map(), urls = new Set();
5
+ const authenticated = fetch('/api/browser/session', {method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}).then(async r=>{if(!r.ok)throw Error((await r.json()).message);});
6
+ async function api(path, data, binary=false) {
7
+ await authenticated;
8
+ const options={headers:{}};
9
+ if(data!==undefined){
10
+ const signature=path+JSON.stringify(data);
11
+ if(!attempts.has(signature))attempts.set(signature,crypto.randomUUID());
12
+ options.method='POST';options.headers={'Content-Type':'application/json','Idempotency-Key':attempts.get(signature)};options.body=JSON.stringify(data);
13
+ }
14
+ const response=await fetch(path,options);
15
+ if(!response.ok){const error=await response.json();throw Error(error.message||error.error);}
16
+ const result=binary?await response.blob():await response.json();
17
+ if(data!==undefined)attempts.delete(path+JSON.stringify(data));
18
+ return result;
19
+ }
20
+ function sync(){el('text').disabled=!selected||busy;el('send').disabled=!selected||busy||!el('text').value.trim();el('send-file').disabled=!selected||busy||!el('file').files.length;el('history').disabled=!selected||busy;}
21
+ async function action(task){try{await task();}catch(error){el('status').textContent=error.message;}}
22
+ function renderChats(){
23
+ el('chats').replaceChildren();const q=el('search').value.toLocaleLowerCase();
24
+ for(const chat of chats){
25
+ if(!((chat.displayName||chat.displayString||'')+' '+chat.id).toLocaleLowerCase().includes(q))continue;
26
+ const button=document.createElement('button'),name=document.createElement('strong'),meta=document.createElement('small');
27
+ button.type='button';button.disabled=busy||chat.chatType==='folder';button.setAttribute('aria-current',String(chat.id===selected));
28
+ name.textContent=(chat.isPinned?'📌 ':'')+(chat.displayName||chat.displayString||chat.id);
29
+ meta.textContent=chat.lastMessageTime?new Date(chat.lastMessageTime*1000).toLocaleString():chat.id;
30
+ button.append(name,meta);button.onclick=()=>action(async()=>{selected=chat.id;el('title').textContent=name.textContent;renderChats();sync();await history();});el('chats').append(button);
31
+ }
32
+ }
33
+ async function refresh(){const result=await api('/api/native/chats');chats=result.items;renderChats();el('status').textContent=`${chats.length} conversations. Pinned first, then recent.`;}
34
+ function button(article,label,task){const b=document.createElement('button');b.type='button';b.textContent=label;b.onclick=()=>action(async()=>{b.disabled=true;try{await task();}finally{b.disabled=false;}});article.append(b);}
35
+ async function history(){
36
+ const chatId=selected,request=++sequence;if(!chatId)return;
37
+ const result=await api('/api/native/messages?'+new URLSearchParams({chatId,limit:el('limit').value}));
38
+ if(request!==sequence||chatId!==selected)return;
39
+ for(const url of urls)URL.revokeObjectURL(url);urls.clear();el('messages').replaceChildren();
40
+ for(const message of result.items){
41
+ const article=document.createElement('article'),meta=document.createElement('small'),text=document.createElement('p');
42
+ meta.textContent=(message.senderName||message.groupSender||message.from||'Unknown sender')+' · '+(message.timestamp?new Date(message.timestamp*1000).toLocaleString():'')+' · '+message.typeLabel;
43
+ text.textContent=message.displaySummary||message.summary;article.append(meta,text);
44
+ const params={chatId,id:message.id};
45
+ if(['image','sticker','video','voice','file'].includes(message.kind))button(article,message.kind==='file'?'Download file':'Load media',async()=>{
46
+ const blob=await api('/api/native/media?'+new URLSearchParams(params),undefined,true);
47
+ if(request!==sequence)return;
48
+ const url=URL.createObjectURL(blob);urls.add(url);
49
+ if(blob.type.startsWith('image/')){const img=document.createElement('img');img.src=url;img.alt=message.typeLabel;article.append(img);}
50
+ else if(blob.type.startsWith('audio/')||blob.type.startsWith('video/')){const player=document.createElement(blob.type.startsWith('audio/')?'audio':'video');player.controls=true;player.src=url;article.append(player);}
51
+ else{const link=document.createElement('a');link.href=url;link.download=message.details.fileName||'attachment';link.textContent='Save '+link.download;article.append(link);link.click();}
52
+ });
53
+ if(message.kind==='file')button(article,'Fetch in WeChat',async()=>{el('status').textContent=JSON.stringify(await api('/api/native/attachments/download',params));});
54
+ if(message.kind==='image')button(article,'Extract text',async()=>{const r=await api('/api/native/images/text',params);const p=document.createElement('p');p.textContent=r.text;article.append(p);});
55
+ if(message.kind==='voice'){
56
+ button(article,'Transcribe',async()=>{const r=await api('/api/native/voice/transcribe',params);el('status').textContent=r.text||r.status;});
57
+ button(article,'Read transcript',async()=>{const r=await api('/api/native/voice/transcript?'+new URLSearchParams(params));el('status').textContent=r.text||r.status;});
58
+ }
59
+ el('messages').append(article);
60
+ }
61
+ el('status').textContent=`${result.items.length} messages. Newest first.`;
62
+ }
63
+ el('compose').onsubmit=event=>{event.preventDefault();action(async()=>{
64
+ if(busy||!selected||!el('text').value.trim())return;
65
+ busy=true;sync();renderChats();
66
+ try{const r=await api('/api/messages/send',{chatId:selected,text:el('text').value});if(!r.acceptedByClient)throw Error('Check WeChat before retrying.');el('text').value='';await history();el('status').textContent='Queued in WeChat.';}
67
+ finally{busy=false;sync();renderChats();}
68
+ });};
69
+ el('upload').onsubmit=event=>{event.preventDefault();action(async()=>{
70
+ const file=el('file').files[0];if(busy||!selected||!file)return;if(file.size>8*1024*1024)throw Error('Uploads must be at most 8 MiB.');
71
+ busy=true;sync();renderChats();
72
+ try{const bytes=new Uint8Array(await file.arrayBuffer());let encoded='';for(let i=0;i<bytes.length;i+=16384)encoded+=String.fromCharCode(...bytes.subarray(i,i+16384));
73
+ const kind=el('file-kind').value;const data={chatId:selected,dataBase64:btoa(encoded),...(kind==='file'?{name:file.name}:{format:kind})};
74
+ await api('/api/messages/'+(kind==='file'?'file':'voice'),data);el('file').value='';await history();el('status').textContent='Upload queued in WeChat.';
75
+ }finally{busy=false;sync();renderChats();}
76
+ });};
77
+ el('refresh').onclick=()=>action(refresh);el('history').onclick=()=>action(history);el('connection').onclick=()=>action(async()=>{el('status').textContent=JSON.stringify(await api('/api/native/status'),null,2);});
78
+ el('search').oninput=renderChats;el('text').oninput=sync;el('file').onchange=sync;el('limit').onchange=()=>action(history);
79
+ action(refresh);