weflow-cli 1.5.0 → 1.6.0
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.
- package/ARCHITECTURE.md +105 -0
- package/CHANGELOG.md +110 -0
- package/CONTRIBUTING.md +45 -0
- package/LICENSE +21 -21
- package/OPERATIONS.md +272 -0
- package/README.en.md +281 -0
- package/README.md +333 -149
- package/SECURITY.md +47 -0
- package/bin/weflow-cli.ts +4032 -583
- package/dist/bin/weflow-cli.js +4296 -593
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/index.js +178 -236
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/core/dbPathService.d.ts +15 -1
- package/dist/src/core/dbPathService.d.ts.map +1 -1
- package/dist/src/core/dbPathService.js +177 -19
- package/dist/src/core/dbPathService.js.map +1 -1
- package/dist/src/core/keyService.d.ts +8 -0
- package/dist/src/core/keyService.d.ts.map +1 -1
- package/dist/src/core/keyService.js +127 -22
- package/dist/src/core/keyService.js.map +1 -1
- package/dist/src/core/ntCore.d.ts +59 -1
- package/dist/src/core/ntCore.d.ts.map +1 -1
- package/dist/src/core/ntCore.js +188 -64
- package/dist/src/core/ntCore.js.map +1 -1
- package/dist/src/core/sqlcipherCore.d.ts.map +1 -1
- package/dist/src/core/sqlcipherCore.js +18 -1
- package/dist/src/core/sqlcipherCore.js.map +1 -1
- package/dist/src/core/wcdbCore.d.ts.map +1 -1
- package/dist/src/core/wcdbCore.js +3 -6
- package/dist/src/core/wcdbCore.js.map +1 -1
- package/dist/src/services/assistantDaemon.d.ts +19 -0
- package/dist/src/services/assistantDaemon.d.ts.map +1 -0
- package/dist/src/services/assistantDaemon.js +117 -0
- package/dist/src/services/assistantDaemon.js.map +1 -0
- package/dist/src/services/assistantMemory.d.ts +36 -0
- package/dist/src/services/assistantMemory.d.ts.map +1 -0
- package/dist/src/services/assistantMemory.js +182 -0
- package/dist/src/services/assistantMemory.js.map +1 -0
- package/dist/src/services/assistantPrivacy.d.ts +25 -0
- package/dist/src/services/assistantPrivacy.d.ts.map +1 -0
- package/dist/src/services/assistantPrivacy.js +77 -0
- package/dist/src/services/assistantPrivacy.js.map +1 -0
- package/dist/src/services/assistantRouting.d.ts +18 -0
- package/dist/src/services/assistantRouting.d.ts.map +1 -0
- package/dist/src/services/assistantRouting.js +68 -0
- package/dist/src/services/assistantRouting.js.map +1 -0
- package/dist/src/services/assistantService.d.ts +37 -0
- package/dist/src/services/assistantService.d.ts.map +1 -0
- package/dist/src/services/assistantService.js +284 -0
- package/dist/src/services/assistantService.js.map +1 -0
- package/dist/src/services/assistantTools.d.ts +24 -0
- package/dist/src/services/assistantTools.d.ts.map +1 -0
- package/dist/src/services/assistantTools.js +582 -0
- package/dist/src/services/assistantTools.js.map +1 -0
- package/dist/src/services/chatService.d.ts +44 -0
- package/dist/src/services/chatService.d.ts.map +1 -1
- package/dist/src/services/chatService.js +105 -4
- package/dist/src/services/chatService.js.map +1 -1
- package/dist/src/services/configService.d.ts +55 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +147 -6
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/evidenceService.d.ts +30 -0
- package/dist/src/services/evidenceService.d.ts.map +1 -0
- package/dist/src/services/evidenceService.js +98 -0
- package/dist/src/services/evidenceService.js.map +1 -0
- package/dist/src/services/exportService.d.ts +13 -20
- package/dist/src/services/exportService.d.ts.map +1 -1
- package/dist/src/services/exportService.js +113 -47
- package/dist/src/services/exportService.js.map +1 -1
- package/dist/src/services/initKeyService.d.ts +19 -0
- package/dist/src/services/initKeyService.d.ts.map +1 -0
- package/dist/src/services/initKeyService.js +80 -0
- package/dist/src/services/initKeyService.js.map +1 -0
- package/dist/src/services/messageContract.d.ts +23 -0
- package/dist/src/services/messageContract.d.ts.map +1 -0
- package/dist/src/services/messageContract.js +49 -0
- package/dist/src/services/messageContract.js.map +1 -0
- package/dist/src/services/messageQuery.d.ts +7 -0
- package/dist/src/services/messageQuery.d.ts.map +1 -0
- package/dist/src/services/messageQuery.js +30 -0
- package/dist/src/services/messageQuery.js.map +1 -0
- package/dist/src/services/wechat-formatter.js +14 -14
- package/dist/src/services/wechatMessageService.d.ts.map +1 -1
- package/dist/src/services/wechatMessageService.js +31 -3
- package/dist/src/services/wechatMessageService.js.map +1 -1
- package/dist/src/services/whitelistService.d.ts +52 -4
- package/dist/src/services/whitelistService.d.ts.map +1 -1
- package/dist/src/services/whitelistService.js +183 -17
- package/dist/src/services/whitelistService.js.map +1 -1
- package/dist/src/types.d.ts +20 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/dateRange.d.ts +15 -0
- package/dist/src/utils/dateRange.d.ts.map +1 -0
- package/dist/src/utils/dateRange.js +48 -0
- package/dist/src/utils/dateRange.js.map +1 -0
- package/dist/src/utils/mcpSecurity.d.ts +5 -0
- package/dist/src/utils/mcpSecurity.d.ts.map +1 -0
- package/dist/src/utils/mcpSecurity.js +39 -0
- package/dist/src/utils/mcpSecurity.js.map +1 -0
- package/dist/src/utils/packageRoot.d.ts +2 -0
- package/dist/src/utils/packageRoot.d.ts.map +1 -0
- package/dist/src/utils/packageRoot.js +16 -0
- package/dist/src/utils/packageRoot.js.map +1 -0
- package/dist/src/utils/python.d.ts +2 -0
- package/dist/src/utils/python.d.ts.map +1 -0
- package/dist/src/utils/python.js +72 -0
- package/dist/src/utils/python.js.map +1 -0
- package/dist/src/utils/pythonProcessEnv.d.ts +3 -0
- package/dist/src/utils/pythonProcessEnv.d.ts.map +1 -0
- package/dist/src/utils/pythonProcessEnv.js +46 -0
- package/dist/src/utils/pythonProcessEnv.js.map +1 -0
- package/dist/src/utils/talkerUtils.d.ts +4 -1
- package/dist/src/utils/talkerUtils.d.ts.map +1 -1
- package/dist/src/utils/talkerUtils.js +9 -9
- package/dist/src/utils/talkerUtils.js.map +1 -1
- package/dist/src/utils/wechatEmoji.d.ts +5 -0
- package/dist/src/utils/wechatEmoji.d.ts.map +1 -0
- package/dist/src/utils/wechatEmoji.js +35 -0
- package/dist/src/utils/wechatEmoji.js.map +1 -0
- package/docs/AI_INTERFACE.md +189 -0
- package/docs/BRANCHES.md +87 -0
- package/docs/DATA_CONTRACT.md +74 -0
- package/docs/DECISIONS.md +270 -0
- package/docs/DEEPSEEK_V4_FLASH/346/226/275/345/267/245/346/226/207/346/241/243.md +234 -0
- package/docs/EVIDENCE_GUIDE.md +96 -0
- package/docs/MCP.md +95 -0
- package/docs/PARTNERS.md +41 -0
- package/docs/PROJECT_STATE.md +96 -0
- package/docs/ROADMAP.md +191 -0
- package/docs/SETUP.md +103 -0
- package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
- package/docs/images/weflow-architecture.png +0 -0
- package/docs/images/weflow-architecture.svg +65 -0
- package/mcp-server/index.ts +169 -269
- package/package.json +81 -72
- package/requirements-3x.txt +4 -0
- package/requirements-voice.txt +15 -0
- package/requirements.txt +9 -0
- package/resources/js/marked.min.js +69 -0
- package/resources/wechat-emoji/666.png +0 -0
- package/resources/wechat-emoji/Awesome.png +0 -0
- package/resources/wechat-emoji/Concerned.png +0 -0
- package/resources/wechat-emoji/Cry.png +0 -0
- package/resources/wechat-emoji/Emm.png +0 -0
- package/resources/wechat-emoji/Facepalm.png +0 -0
- package/resources/wechat-emoji/Grin.png +0 -0
- package/resources/wechat-emoji/OK.png +0 -0
- package/resources/wechat-emoji/Respect.png +0 -0
- package/resources/wechat-emoji/Sick.png +0 -0
- package/resources/wechat-emoji/Sleep.png +0 -0
- package/resources/wechat-emoji/Smile.png +0 -0
- package/resources/wechat-emoji//344/272/262/344/272/262.png +0 -0
- package/resources/wechat-emoji//344/276/277/344/276/277.png +0 -0
- package/resources/wechat-emoji//345/201/267/347/254/221.png +0 -0
- package/resources/wechat-emoji//345/202/262/346/205/242.png +0 -0
- package/resources/wechat-emoji//345/206/215/350/247/201.png +0 -0
- package/resources/wechat-emoji//345/207/213/350/260/242.png +0 -0
- package/resources/wechat-emoji//345/212/240/346/262/271.png +0 -0
- package/resources/wechat-emoji//345/213/276/345/274/225.png +0 -0
- package/resources/wechat-emoji//345/217/221/345/221/206.png +0 -0
- package/resources/wechat-emoji//345/217/221/346/200/222.png +0 -0
- package/resources/wechat-emoji//345/217/221/346/212/226.png +0 -0
- package/resources/wechat-emoji//345/217/257/346/200/234.png +0 -0
- package/resources/wechat-emoji//345/217/263/345/223/274/345/223/274.png +0 -0
- package/resources/wechat-emoji//345/217/271/346/260/224.png +0 -0
- package/resources/wechat-emoji//345/220/203/347/223/234.png +0 -0
- package/resources/wechat-emoji//345/220/210/345/215/201.png +0 -0
- package/resources/wechat-emoji//345/220/220.png +0 -0
- package/resources/wechat-emoji//345/221/262/347/211/231.png +0 -0
- package/resources/wechat-emoji//345/222/222/351/252/202.png +0 -0
- package/resources/wechat-emoji//345/222/226/345/225/241.png +0 -0
- package/resources/wechat-emoji//345/223/207.png +0 -0
- package/resources/wechat-emoji//345/225/244/351/205/222.png +0 -0
- package/resources/wechat-emoji//345/230/230.png +0 -0
- package/resources/wechat-emoji//345/230/264/345/224/207.png +0 -0
- package/resources/wechat-emoji//345/230/277/345/223/210.png +0 -0
- package/resources/wechat-emoji//345/233/247.png +0 -0
- package/resources/wechat-emoji//345/233/260.png +0 -0
- package/resources/wechat-emoji//345/235/217/347/254/221.png +0 -0
- package/resources/wechat-emoji//345/244/247/345/223/255.png +0 -0
- package/resources/wechat-emoji//345/244/251/345/225/212.png +0 -0
- package/resources/wechat-emoji//345/244/252/351/230/263.png +0 -0
- package/resources/wechat-emoji//345/244/261/346/234/233.png +0 -0
- package/resources/wechat-emoji//345/245/270/347/254/221.png +0 -0
- package/resources/wechat-emoji//345/245/275/347/232/204.png +0 -0
- package/resources/wechat-emoji//345/247/224/345/261/210.png +0 -0
- package/resources/wechat-emoji//345/256/263/347/276/236.png +0 -0
- package/resources/wechat-emoji//345/260/264/345/260/254.png +0 -0
- package/resources/wechat-emoji//345/272/206/347/245/235.png +0 -0
- package/resources/wechat-emoji//345/274/261.png +0 -0
- package/resources/wechat-emoji//345/274/272.png +0 -0
- package/resources/wechat-emoji//345/276/227/346/204/217.png +0 -0
- package/resources/wechat-emoji//345/276/256/347/254/221.png +0 -0
- package/resources/wechat-emoji//345/277/203/347/242/216.png +0 -0
- package/resources/wechat-emoji//345/277/253/345/223/255/344/272/206.png +0 -0
- package/resources/wechat-emoji//346/201/220/346/203/247.png +0 -0
- package/resources/wechat-emoji//346/202/240/351/227/262.png +0 -0
- package/resources/wechat-emoji//346/203/212/346/201/220.png +0 -0
- package/resources/wechat-emoji//346/203/212/350/256/266.png +0 -0
- package/resources/wechat-emoji//346/204/211/345/277/253.png +0 -0
- package/resources/wechat-emoji//346/206/250/347/254/221.png +0 -0
- package/resources/wechat-emoji//346/211/223/350/204/270.png +0 -0
- package/resources/wechat-emoji//346/212/223/347/213/202.png +0 -0
- package/resources/wechat-emoji//346/212/240/351/274/273.png +0 -0
- package/resources/wechat-emoji//346/212/261/346/213/263.png +0 -0
- package/resources/wechat-emoji//346/213/245/346/212/261.png +0 -0
- package/resources/wechat-emoji//346/213/263/345/244/264.png +0 -0
- package/resources/wechat-emoji//346/215/202/350/204/270.png +0 -0
- package/resources/wechat-emoji//346/217/241/346/211/213.png +0 -0
- package/resources/wechat-emoji//346/222/207/345/230/264.png +0 -0
- package/resources/wechat-emoji//346/223/246/346/261/227.png +0 -0
- package/resources/wechat-emoji//346/225/262/346/211/223.png +0 -0
- package/resources/wechat-emoji//346/227/240/350/257/255.png +0 -0
- package/resources/wechat-emoji//346/227/272/346/237/264.png +0 -0
- package/resources/wechat-emoji//346/231/225.png +0 -0
- package/resources/wechat-emoji//346/234/210/344/272/256.png +0 -0
- package/resources/wechat-emoji//346/234/272/346/231/272.png +0 -0
- package/resources/wechat-emoji//346/261/227.png +0 -0
- package/resources/wechat-emoji//346/265/201/346/263/252.png +0 -0
- package/resources/wechat-emoji//347/202/270/345/274/271.png +0 -0
- package/resources/wechat-emoji//347/203/237/350/212/261.png +0 -0
- package/resources/wechat-emoji//347/210/206/347/253/271.png +0 -0
- package/resources/wechat-emoji//347/210/261/345/277/203.png +0 -0
- package/resources/wechat-emoji//347/214/252/345/244/264.png +0 -0
- package/resources/wechat-emoji//347/216/253/347/221/260.png +0 -0
- package/resources/wechat-emoji//347/224/237/347/227/205.png +0 -0
- package/resources/wechat-emoji//347/226/221/351/227/256.png +0 -0
- package/resources/wechat-emoji//347/231/274.png +0 -0
- package/resources/wechat-emoji//347/231/275/347/234/274.png +0 -0
- package/resources/wechat-emoji//347/232/261/347/234/211.png +0 -0
- package/resources/wechat-emoji//347/235/241.png +0 -0
- package/resources/wechat-emoji//347/240/264/346/266/225/344/270/272/347/254/221.png +0 -0
- package/resources/wechat-emoji//347/244/274/347/211/251.png +0 -0
- package/resources/wechat-emoji//347/244/276/344/274/232/347/244/276/344/274/232.png +0 -0
- package/resources/wechat-emoji//347/246/217.png +0 -0
- package/resources/wechat-emoji//347/254/221/350/204/270.png +0 -0
- package/resources/wechat-emoji//347/272/242/345/214/205.png +0 -0
- package/resources/wechat-emoji//347/277/273/347/231/275/347/234/274.png +0 -0
- package/resources/wechat-emoji//350/200/266.png +0 -0
- package/resources/wechat-emoji//350/203/234/345/210/251.png +0 -0
- package/resources/wechat-emoji//350/204/270/347/272/242.png +0 -0
- package/resources/wechat-emoji//350/211/262.png +0 -0
- package/resources/wechat-emoji//350/213/246/346/266/251.png +0 -0
- package/resources/wechat-emoji//350/217/234/345/210/200.png +0 -0
- package/resources/wechat-emoji//350/233/213/347/263/225.png +0 -0
- package/resources/wechat-emoji//350/241/260.png +0 -0
- package/resources/wechat-emoji//350/243/202/345/274/200.png +0 -0
- package/resources/wechat-emoji//350/256/251/346/210/221/347/234/213/347/234/213.png +0 -0
- package/resources/wechat-emoji//350/260/203/347/232/256.png +0 -0
- package/resources/wechat-emoji//350/267/263/350/267/263.png +0 -0
- package/resources/wechat-emoji//350/275/254/345/234/210.png +0 -0
- package/resources/wechat-emoji//351/204/231/350/247/206.png +0 -0
- package/resources/wechat-emoji//351/227/255/345/230/264.png +0 -0
- package/resources/wechat-emoji//351/230/264/351/231/251.png +0 -0
- package/resources/wechat-emoji//351/232/276/350/277/207.png +0 -0
- package/resources/wechat-emoji//351/252/267/351/253/205.png +0 -0
- package/resources/wechat-emoji//351/274/223/346/216/214.png +0 -0
- package/scripts/_batch_link_sources.py +73 -73
- package/scripts/_utils.py +29 -0
- package/scripts/annual_report.py +796 -796
- package/scripts/auto_tag.py +128 -128
- package/scripts/biz_daily.py +210 -53
- package/scripts/chat_report.py +10 -0
- package/scripts/chat_stats.py +702 -702
- package/scripts/classify_daily.py +17 -11
- package/scripts/create_reading_notes.py +370 -370
- package/scripts/daily_stats.py +132 -0
- package/scripts/enrich_backlinks.py +163 -163
- package/scripts/export_chat_html.py +1704 -107
- package/scripts/extract_todos.py +445 -404
- package/scripts/fav_server.py +119 -12
- package/scripts/fix_topics.py +111 -111
- package/scripts/generate_ai_report.py +1 -1
- package/scripts/generate_html.py +597 -277
- package/scripts/inspection_records.py +354 -0
- package/scripts/mcp_bridge.py +398 -398
- package/scripts/nt_decrypt.py +693 -48
- package/scripts/pipeline.py +27 -19
- package/scripts/promote_all.py +312 -310
- package/scripts/promote_ideas.py +299 -297
- package/scripts/rag_chat.py +194 -191
- package/scripts/semantic_search.py +435 -431
- package/scripts/sync_fav.py +201 -146
- package/scripts/sync_weread.py +192 -192
- package/scripts/vault_rag.py +140 -139
- package/scripts/vault_search.py +141 -141
- package/scripts/wechat_emoji.py +132 -0
- package/scripts/wechat_emoticon.py +433 -0
- package/scripts/wechat_image.py +258 -0
- package/scripts/wechat_voice.py +327 -0
- package/src/core/dbPathService.ts +163 -18
- package/src/core/keyService.ts +137 -25
- package/src/core/ntCore.ts +202 -61
- package/src/core/sqlcipherCore.ts +17 -1
- package/src/core/wcdbCore.ts +3 -7
- package/src/services/assistantDaemon.ts +111 -0
- package/src/services/assistantMemory.ts +193 -0
- package/src/services/assistantPrivacy.ts +87 -0
- package/src/services/assistantRouting.ts +92 -0
- package/src/services/assistantService.ts +295 -0
- package/src/services/assistantTools.ts +549 -0
- package/src/services/chatService.ts +133 -4
- package/src/services/configService.ts +194 -6
- package/src/services/evidenceService.ts +129 -0
- package/src/services/exportService.ts +113 -48
- package/src/services/initKeyService.ts +83 -0
- package/src/services/messageContract.ts +77 -0
- package/src/services/messageQuery.ts +38 -0
- package/src/services/wechat-formatter.ts +348 -348
- package/src/services/wechatMessageService.ts +25 -3
- package/src/services/wereadService.ts +308 -308
- package/src/services/whitelistService.ts +209 -22
- package/src/types.ts +20 -0
- package/src/utils/dateRange.ts +58 -0
- package/src/utils/mcpSecurity.ts +39 -0
- package/src/utils/packageRoot.ts +14 -0
- package/src/utils/python.ts +73 -0
- package/src/utils/pythonProcessEnv.ts +45 -0
- package/src/utils/talkerUtils.ts +14 -2
- package/dist/src/utils/errors.d.ts +0 -25
- package/dist/src/utils/errors.d.ts.map +0 -1
- package/dist/src/utils/errors.js +0 -43
- package/dist/src/utils/errors.js.map +0 -1
- package/dist/src/utils/pythonRunner.d.ts +0 -27
- package/dist/src/utils/pythonRunner.d.ts.map +0 -1
- package/dist/src/utils/pythonRunner.js +0 -62
- package/dist/src/utils/pythonRunner.js.map +0 -1
- package/src/utils/errors.ts +0 -42
- package/src/utils/pythonRunner.ts +0 -81
package/scripts/nt_decrypt.py
CHANGED
|
@@ -11,11 +11,20 @@ import ctypes
|
|
|
11
11
|
from ctypes import wintypes, c_void_p, c_size_t, create_string_buffer, byref, sizeof
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
sqlcipher = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def require_sqlcipher():
|
|
18
|
+
"""Load SQLCipher only for database operations, not path discovery."""
|
|
19
|
+
global sqlcipher
|
|
20
|
+
if sqlcipher is not None:
|
|
21
|
+
return sqlcipher
|
|
22
|
+
try:
|
|
23
|
+
from sqlcipher3 import dbapi2 as sqlcipher_module
|
|
24
|
+
except ImportError as error:
|
|
25
|
+
raise RuntimeError("需要 sqlcipher3: pip install sqlcipher3") from error
|
|
26
|
+
sqlcipher = sqlcipher_module
|
|
27
|
+
return sqlcipher
|
|
19
28
|
|
|
20
29
|
# ========== Memory Scanner ==========
|
|
21
30
|
PROCESS_VM_READ = 0x0010
|
|
@@ -36,17 +45,24 @@ class MEMORY_BASIC_INFORMATION(ctypes.Structure):
|
|
|
36
45
|
('Type', wintypes.DWORD),
|
|
37
46
|
]
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
IS_WINDOWS = os.name == 'nt'
|
|
49
|
+
|
|
50
|
+
if IS_WINDOWS:
|
|
51
|
+
kernel32 = ctypes.windll.kernel32
|
|
52
|
+
ReadProcessMemory = kernel32.ReadProcessMemory
|
|
53
|
+
ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.LPVOID, ctypes.c_size_t, ctypes.POINTER(c_size_t)]
|
|
54
|
+
ReadProcessMemory.restype = wintypes.BOOL
|
|
55
|
+
VirtualQueryEx = kernel32.VirtualQueryEx
|
|
56
|
+
VirtualQueryEx.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, ctypes.c_void_p, ctypes.c_size_t]
|
|
57
|
+
VirtualQueryEx.restype = ctypes.c_size_t
|
|
58
|
+
else:
|
|
59
|
+
kernel32 = None
|
|
46
60
|
|
|
47
61
|
|
|
48
62
|
def find_weixin_pid():
|
|
49
|
-
"""Find
|
|
63
|
+
"""Find WeChat process ID (Windows: pymem; Linux: /proc scan)."""
|
|
64
|
+
if not IS_WINDOWS:
|
|
65
|
+
return find_weixin_pid_linux()
|
|
50
66
|
try:
|
|
51
67
|
import pymem, pymem.process
|
|
52
68
|
for proc in pymem.process.list_processes():
|
|
@@ -63,11 +79,67 @@ def find_weixin_pid():
|
|
|
63
79
|
return None
|
|
64
80
|
|
|
65
81
|
|
|
82
|
+
_LINUX_WECHAT_COMMS = {'wechat', 'wechatappex', 'weixin'}
|
|
83
|
+
_LINUX_EXE_PREFIX_DENY = ('python', 'bash', 'sh', 'zsh', 'node', 'perl', 'ruby', 'electron')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _is_linux_wechat_process(pid):
|
|
87
|
+
if pid == os.getpid():
|
|
88
|
+
return False
|
|
89
|
+
try:
|
|
90
|
+
with open(f'/proc/{pid}/comm') as f:
|
|
91
|
+
comm = f.read().strip().lower()
|
|
92
|
+
if comm in _LINUX_WECHAT_COMMS:
|
|
93
|
+
return True
|
|
94
|
+
try:
|
|
95
|
+
exe = os.path.realpath(os.readlink(f'/proc/{pid}/exe'))
|
|
96
|
+
except OSError:
|
|
97
|
+
return False
|
|
98
|
+
name = os.path.basename(exe).lower()
|
|
99
|
+
if any(name.startswith(p) for p in _LINUX_EXE_PREFIX_DENY):
|
|
100
|
+
return False
|
|
101
|
+
return 'wechat' in name or 'weixin' in name
|
|
102
|
+
except (PermissionError, FileNotFoundError, ProcessLookupError):
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def find_weixin_pid_linux():
|
|
107
|
+
"""Find Linux WeChat main process (largest RSS among candidates)."""
|
|
108
|
+
best = None
|
|
109
|
+
best_rss = -1
|
|
110
|
+
try:
|
|
111
|
+
pids = os.listdir('/proc')
|
|
112
|
+
except OSError:
|
|
113
|
+
return None
|
|
114
|
+
for pid_str in pids:
|
|
115
|
+
if not pid_str.isdigit():
|
|
116
|
+
continue
|
|
117
|
+
pid = int(pid_str)
|
|
118
|
+
if not _is_linux_wechat_process(pid):
|
|
119
|
+
continue
|
|
120
|
+
try:
|
|
121
|
+
with open(f'/proc/{pid}/statm') as f:
|
|
122
|
+
rss_kb = int(f.read().split()[1]) * 4
|
|
123
|
+
except (OSError, IndexError, ValueError):
|
|
124
|
+
rss_kb = 0
|
|
125
|
+
if rss_kb > best_rss:
|
|
126
|
+
best_rss = rss_kb
|
|
127
|
+
best = pid
|
|
128
|
+
return best
|
|
129
|
+
|
|
130
|
+
|
|
66
131
|
def scan_memory_keys(pid):
|
|
67
|
-
"""Scan process memory for x'<64hex_key><32hex_salt>' patterns.
|
|
132
|
+
"""Scan process memory for x'<64hex_key><32hex_salt>' patterns.
|
|
133
|
+
|
|
134
|
+
Returns (keys, error): keys is a list of {"key","salt"} dicts,
|
|
135
|
+
error is None on success or 'permission' / 'gone' / 'not_windows'.
|
|
136
|
+
"""
|
|
137
|
+
if not IS_WINDOWS:
|
|
138
|
+
return scan_memory_keys_linux(pid)
|
|
139
|
+
|
|
68
140
|
hProcess = kernel32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
|
|
69
141
|
if not hProcess:
|
|
70
|
-
return []
|
|
142
|
+
return [], None
|
|
71
143
|
|
|
72
144
|
pattern = re.compile(rb"x'([0-9a-fA-F]{64})([0-9a-fA-F]{32})'")
|
|
73
145
|
keys_found = []
|
|
@@ -114,17 +186,128 @@ def scan_memory_keys(pid):
|
|
|
114
186
|
seen.add(pair)
|
|
115
187
|
unique_keys.append({"key": k, "salt": s})
|
|
116
188
|
|
|
117
|
-
return unique_keys
|
|
189
|
+
return unique_keys, None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
_LINUX_SKIP_MAPPINGS = {'[vdso]', '[vsyscall]', '[vvar]'}
|
|
193
|
+
_LINUX_SKIP_PREFIXES = ('/usr/lib/', '/lib/', '/usr/share/')
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def scan_memory_keys_linux(pid):
|
|
197
|
+
"""Scan /proc/<pid>/maps + /proc/<pid>/mem for the key pattern.
|
|
198
|
+
|
|
199
|
+
Requires root or CAP_SYS_PTRACE (or the target being a descendant
|
|
200
|
+
of this process when yama ptrace_scope=1).
|
|
201
|
+
"""
|
|
202
|
+
regions = []
|
|
203
|
+
try:
|
|
204
|
+
with open(f'/proc/{pid}/maps') as f:
|
|
205
|
+
for line in f:
|
|
206
|
+
parts = line.split()
|
|
207
|
+
if len(parts) < 2 or 'r' not in parts[1]:
|
|
208
|
+
continue
|
|
209
|
+
if len(parts) >= 6:
|
|
210
|
+
name = parts[5]
|
|
211
|
+
if name in _LINUX_SKIP_MAPPINGS:
|
|
212
|
+
continue
|
|
213
|
+
name_lower = name.lower()
|
|
214
|
+
if name.startswith(_LINUX_SKIP_PREFIXES) and \
|
|
215
|
+
'wcdb' not in name_lower and 'wechat' not in name_lower and 'weixin' not in name_lower:
|
|
216
|
+
continue
|
|
217
|
+
try:
|
|
218
|
+
start_s, end_s = parts[0].split('-')
|
|
219
|
+
start = int(start_s, 16)
|
|
220
|
+
size = int(end_s, 16) - start
|
|
221
|
+
except ValueError:
|
|
222
|
+
continue
|
|
223
|
+
if 0 < size < 500 * 1024 * 1024:
|
|
224
|
+
regions.append((start, size))
|
|
225
|
+
except PermissionError:
|
|
226
|
+
return [], 'permission'
|
|
227
|
+
except (FileNotFoundError, ProcessLookupError):
|
|
228
|
+
return [], 'gone'
|
|
229
|
+
|
|
230
|
+
pattern = re.compile(rb"x'([0-9a-fA-F]{64})([0-9a-fA-F]{32})'")
|
|
231
|
+
keys_found = []
|
|
232
|
+
try:
|
|
233
|
+
with open(f'/proc/{pid}/mem', 'rb') as mem:
|
|
234
|
+
for base, size in regions:
|
|
235
|
+
try:
|
|
236
|
+
mem.seek(base)
|
|
237
|
+
data = mem.read(size)
|
|
238
|
+
except (OSError, ValueError):
|
|
239
|
+
continue
|
|
240
|
+
for m in pattern.finditer(data):
|
|
241
|
+
keys_found.append((m.group(1).decode(), m.group(2).decode()))
|
|
242
|
+
except PermissionError:
|
|
243
|
+
return [], 'permission'
|
|
244
|
+
except (FileNotFoundError, ProcessLookupError):
|
|
245
|
+
return [], 'gone'
|
|
246
|
+
|
|
247
|
+
seen = set()
|
|
248
|
+
unique_keys = []
|
|
249
|
+
for k, s in keys_found:
|
|
250
|
+
pair = (k, s)
|
|
251
|
+
if pair not in seen:
|
|
252
|
+
seen.add(pair)
|
|
253
|
+
unique_keys.append({"key": k, "salt": s})
|
|
254
|
+
|
|
255
|
+
return unique_keys, None
|
|
118
256
|
|
|
119
257
|
|
|
120
258
|
# ========== NT Database Discovery ==========
|
|
121
259
|
|
|
122
|
-
def
|
|
260
|
+
def _is_nt_account_dir(path):
|
|
261
|
+
return os.path.isdir(os.path.join(path, 'db_storage')) or \
|
|
262
|
+
os.path.isdir(os.path.join(path, 'Msg'))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _normalize_nt_root(root):
|
|
266
|
+
if not root:
|
|
267
|
+
return None
|
|
268
|
+
path = os.path.abspath(os.path.expandvars(os.path.expanduser(root)))
|
|
269
|
+
if os.path.isfile(path):
|
|
270
|
+
path = os.path.dirname(path)
|
|
271
|
+
if not os.path.isdir(path):
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
for entry in os.listdir(path):
|
|
276
|
+
candidate = os.path.join(path, entry)
|
|
277
|
+
if os.path.isdir(candidate) and _is_nt_account_dir(candidate):
|
|
278
|
+
return path
|
|
279
|
+
except OSError:
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
for _ in range(6):
|
|
283
|
+
if _is_nt_account_dir(path):
|
|
284
|
+
return os.path.dirname(path)
|
|
285
|
+
parent = os.path.dirname(path)
|
|
286
|
+
if parent == path:
|
|
287
|
+
break
|
|
288
|
+
path = parent
|
|
289
|
+
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def find_nt_databases(root=None):
|
|
123
294
|
"""Find all NT-format databases under xwechat_files (message + contact)."""
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
295
|
+
if root:
|
|
296
|
+
normalized_root = _normalize_nt_root(root)
|
|
297
|
+
candidates = [normalized_root] if normalized_root else []
|
|
298
|
+
elif IS_WINDOWS:
|
|
299
|
+
candidates = [
|
|
300
|
+
os.path.expandvars(r'%USERPROFILE%\xwechat_files'),
|
|
301
|
+
os.path.expandvars(r'%USERPROFILE%\Documents\xwechat_files'),
|
|
302
|
+
]
|
|
303
|
+
else:
|
|
304
|
+
home = os.path.expanduser('~')
|
|
305
|
+
candidates = [
|
|
306
|
+
os.path.join(home, '.local', 'share', 'xwechat_files'),
|
|
307
|
+
os.path.join(home, 'xwechat_files'),
|
|
308
|
+
os.path.join(home, 'Documents', 'xwechat_files'),
|
|
309
|
+
os.path.join(home, '文档', 'xwechat_files'),
|
|
310
|
+
]
|
|
128
311
|
xwechat = None
|
|
129
312
|
for c in candidates:
|
|
130
313
|
if os.path.isdir(c):
|
|
@@ -186,6 +369,22 @@ def find_nt_databases():
|
|
|
186
369
|
except:
|
|
187
370
|
pass
|
|
188
371
|
|
|
372
|
+
# Scan favorites (收藏) database
|
|
373
|
+
fav_db = os.path.join(xwechat, wxid_dir, 'db_storage', 'favorite', 'favorite.db')
|
|
374
|
+
if os.path.isfile(fav_db):
|
|
375
|
+
try:
|
|
376
|
+
with open(fav_db, 'rb') as fh:
|
|
377
|
+
salt = fh.read(16)
|
|
378
|
+
databases.append({
|
|
379
|
+
"path": fav_db,
|
|
380
|
+
"name": "favorite/favorite.db",
|
|
381
|
+
"salt": salt.hex(),
|
|
382
|
+
"size": os.path.getsize(fav_db),
|
|
383
|
+
"wxid": wxid_dir,
|
|
384
|
+
})
|
|
385
|
+
except:
|
|
386
|
+
pass
|
|
387
|
+
|
|
189
388
|
return databases
|
|
190
389
|
|
|
191
390
|
|
|
@@ -215,7 +414,7 @@ def load_contact_names(contact_db_path, contact_key_hex, contact_salt_hex):
|
|
|
215
414
|
|
|
216
415
|
try:
|
|
217
416
|
raw_key = f"x'{contact_key_hex}{contact_salt_hex}'"
|
|
218
|
-
conn =
|
|
417
|
+
conn = require_sqlcipher().connect(contact_db_path)
|
|
219
418
|
c = conn.cursor()
|
|
220
419
|
c.execute(f'PRAGMA key = "{raw_key}";')
|
|
221
420
|
|
|
@@ -263,12 +462,138 @@ def match_keys_to_databases(keys, databases):
|
|
|
263
462
|
def connect_nt_db(db_path, key_hex, salt_hex):
|
|
264
463
|
"""Connect to an NT database using sqlcipher3."""
|
|
265
464
|
raw_key = f"x'{key_hex}{salt_hex}'"
|
|
266
|
-
conn =
|
|
465
|
+
conn = require_sqlcipher().connect(db_path)
|
|
267
466
|
c = conn.cursor()
|
|
268
467
|
c.execute(f'PRAGMA key = "{raw_key}";')
|
|
269
468
|
return conn, c
|
|
270
469
|
|
|
271
470
|
|
|
471
|
+
def get_fav_schema(db_path, key_hex):
|
|
472
|
+
"""Dump favorite.db schema + sample rows (key verification / exploration)."""
|
|
473
|
+
try:
|
|
474
|
+
with open(db_path, 'rb') as fh:
|
|
475
|
+
salt = fh.read(16).hex()
|
|
476
|
+
conn = require_sqlcipher().connect(db_path)
|
|
477
|
+
conn.execute(f'PRAGMA key = "x\'{key_hex}{salt}\'";')
|
|
478
|
+
tables = conn.execute(
|
|
479
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
|
480
|
+
).fetchall()
|
|
481
|
+
result = {"success": True, "tables": [t[0] for t in tables]}
|
|
482
|
+
if not tables:
|
|
483
|
+
result["error"] = "数据库为空或密钥错误"
|
|
484
|
+
result["success"] = False
|
|
485
|
+
conn.close()
|
|
486
|
+
return result
|
|
487
|
+
except Exception as e:
|
|
488
|
+
return {"success": False, "error": str(e).split('\n')[0][:200]}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
FAV_TYPE_NAMES = {
|
|
492
|
+
1: 'text', # 文字
|
|
493
|
+
2: 'image', # 图片
|
|
494
|
+
4: 'video', # 视频
|
|
495
|
+
5: 'article', # 公众号文章/网页链接
|
|
496
|
+
14: 'chatrecord', # 聊天记录
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def parse_fav_content(content):
|
|
501
|
+
"""Extract display fields (title/link/desc/source/cover) from favitem XML."""
|
|
502
|
+
out = {}
|
|
503
|
+
if not content:
|
|
504
|
+
return out
|
|
505
|
+
import xml.etree.ElementTree as ET
|
|
506
|
+
try:
|
|
507
|
+
root = ET.fromstring(content)
|
|
508
|
+
except ET.ParseError:
|
|
509
|
+
return out
|
|
510
|
+
|
|
511
|
+
def text(path):
|
|
512
|
+
el = root.find(path)
|
|
513
|
+
if el is not None and el.text and el.text.strip():
|
|
514
|
+
return el.text.strip()
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
title = (text('weburlitem/pagetitle')
|
|
518
|
+
or text('datalist/dataitem/datatitle')
|
|
519
|
+
or text('title')
|
|
520
|
+
or text('desc'))
|
|
521
|
+
if title:
|
|
522
|
+
out['title'] = title
|
|
523
|
+
link = (text('weburlitem/clean_url')
|
|
524
|
+
or text('source/link')
|
|
525
|
+
or text('datalist/dataitem/stream_weburl'))
|
|
526
|
+
if link:
|
|
527
|
+
out['link'] = link
|
|
528
|
+
desc = text('weburlitem/pagedesc') or text('datalist/dataitem/datadesc')
|
|
529
|
+
if desc and desc != out.get('title'):
|
|
530
|
+
out['desc'] = desc
|
|
531
|
+
src = text('weburlitem/appmsgshareitem/srcdisplayname')
|
|
532
|
+
if src:
|
|
533
|
+
out['source_name'] = src
|
|
534
|
+
cover = (text('weburlitem/pagethumb_url')
|
|
535
|
+
or text('datalist/dataitem/dataext')
|
|
536
|
+
or text('datalist/dataitem/cdn_thumburl'))
|
|
537
|
+
if cover:
|
|
538
|
+
out['cover'] = cover
|
|
539
|
+
fmt = text('datalist/dataitem/datafmt')
|
|
540
|
+
if fmt:
|
|
541
|
+
out['format'] = fmt
|
|
542
|
+
return out
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def get_favorites(db_path, key_hex, limit=100, offset=0, keyword=None, fav_type=None):
|
|
546
|
+
"""List favorite items from favorite.db with parsed content."""
|
|
547
|
+
try:
|
|
548
|
+
with open(db_path, 'rb') as fh:
|
|
549
|
+
salt = fh.read(16).hex()
|
|
550
|
+
conn = require_sqlcipher().connect(db_path)
|
|
551
|
+
conn.execute(f'PRAGMA key = "x\'{key_hex}{salt}\'";')
|
|
552
|
+
c = conn.cursor()
|
|
553
|
+
|
|
554
|
+
tables = [t[0] for t in c.execute(
|
|
555
|
+
"SELECT name FROM sqlite_master WHERE type='table'").fetchall()]
|
|
556
|
+
if 'fav_db_item' not in tables:
|
|
557
|
+
conn.close()
|
|
558
|
+
return {"error": f"未找到 fav_db_item 表,现有表: {tables[:20]}"}
|
|
559
|
+
|
|
560
|
+
total = c.execute('SELECT count(*) FROM fav_db_item').fetchone()[0]
|
|
561
|
+
|
|
562
|
+
sql = ('SELECT local_id, server_id, type, update_time, fromusr, realchatname, content '
|
|
563
|
+
'FROM fav_db_item WHERE 1=1')
|
|
564
|
+
params = []
|
|
565
|
+
if fav_type is not None:
|
|
566
|
+
sql += ' AND type = ?'
|
|
567
|
+
params.append(fav_type)
|
|
568
|
+
if keyword:
|
|
569
|
+
sql += ' AND content LIKE ?'
|
|
570
|
+
params.append(f'%{keyword}%')
|
|
571
|
+
sql += ' ORDER BY update_time DESC LIMIT ? OFFSET ?'
|
|
572
|
+
params.extend([limit, offset])
|
|
573
|
+
rows = c.execute(sql, params).fetchall()
|
|
574
|
+
conn.close()
|
|
575
|
+
|
|
576
|
+
items = []
|
|
577
|
+
for local_id, server_id, ftype, update_time, fromusr, realchatname, content in rows:
|
|
578
|
+
ct = content if isinstance(content, str) else (
|
|
579
|
+
content.decode('utf-8', errors='replace') if content else '')
|
|
580
|
+
item = {
|
|
581
|
+
'local_id': local_id,
|
|
582
|
+
'server_id': server_id,
|
|
583
|
+
'type': ftype,
|
|
584
|
+
'type_name': FAV_TYPE_NAMES.get(ftype, 'type_%s' % ftype),
|
|
585
|
+
'update_time': update_time,
|
|
586
|
+
'from_user': fromusr,
|
|
587
|
+
'chat_name': realchatname or None,
|
|
588
|
+
}
|
|
589
|
+
item.update(parse_fav_content(ct))
|
|
590
|
+
items.append(item)
|
|
591
|
+
return {"favorites": items, "total": total, "count": len(items),
|
|
592
|
+
"limit": limit, "offset": offset}
|
|
593
|
+
except Exception as e:
|
|
594
|
+
return {"error": str(e).split('\n')[0][:200]}
|
|
595
|
+
|
|
596
|
+
|
|
272
597
|
def get_sessions(conn):
|
|
273
598
|
"""Get chat sessions from NT database (Name2Id table)."""
|
|
274
599
|
c = conn.cursor()
|
|
@@ -439,6 +764,223 @@ def get_contacts(conn, limit=200):
|
|
|
439
764
|
return {"error": str(e)}
|
|
440
765
|
|
|
441
766
|
|
|
767
|
+
# ========== SNS (朋友圈) Queries ==========
|
|
768
|
+
|
|
769
|
+
def parse_sns_content(content_str):
|
|
770
|
+
"""Parse SNS content XML/Protobuf text to extract title, description, media etc."""
|
|
771
|
+
result = {
|
|
772
|
+
'content': '',
|
|
773
|
+
'create_time': 0,
|
|
774
|
+
'username': '',
|
|
775
|
+
'object_id': '',
|
|
776
|
+
'media_count': 0,
|
|
777
|
+
}
|
|
778
|
+
if not content_str:
|
|
779
|
+
return result
|
|
780
|
+
|
|
781
|
+
import re
|
|
782
|
+
|
|
783
|
+
# Extract createTime
|
|
784
|
+
m = re.search(r'<createTime>(\d+)</createTime>', content_str)
|
|
785
|
+
if m:
|
|
786
|
+
result['create_time'] = int(m.group(1))
|
|
787
|
+
|
|
788
|
+
# Extract username
|
|
789
|
+
m = re.search(r'<username>([^<]+)</username>', content_str)
|
|
790
|
+
if m:
|
|
791
|
+
result['username'] = m.group(1)
|
|
792
|
+
|
|
793
|
+
# Extract id
|
|
794
|
+
m = re.search(r'<id>(\d+)</id>', content_str)
|
|
795
|
+
if m:
|
|
796
|
+
result['object_id'] = m.group(1)
|
|
797
|
+
|
|
798
|
+
# Extract contentDesc (main text)
|
|
799
|
+
m = re.search(r'<contentDesc>([^<]*)</contentDesc>', content_str)
|
|
800
|
+
if m:
|
|
801
|
+
result['content'] = m.group(1)
|
|
802
|
+
|
|
803
|
+
# Extract contentDesc CDATA
|
|
804
|
+
m = re.search(r'<contentDesc>\s*<!\[CDATA\[(.*?)\]\]>\s*</contentDesc>', content_str, re.DOTALL)
|
|
805
|
+
if m:
|
|
806
|
+
result['content'] = m.group(1).strip()
|
|
807
|
+
|
|
808
|
+
# Extract title if present
|
|
809
|
+
m = re.search(r'<title>([^<]*)</title>', content_str)
|
|
810
|
+
if m:
|
|
811
|
+
title = m.group(1)
|
|
812
|
+
if title and not result['content']:
|
|
813
|
+
result['content'] = title
|
|
814
|
+
elif title:
|
|
815
|
+
result['content'] = title + '\n' + result['content']
|
|
816
|
+
|
|
817
|
+
# Count media (ContentObject tags)
|
|
818
|
+
result['media_count'] = len(re.findall(r'<ContentObject[ >]', content_str))
|
|
819
|
+
|
|
820
|
+
return result
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def get_sns_timeline(cursor, limit=20, offset=0, usernames=None, keyword=None,
|
|
824
|
+
start_time=None, end_time=None):
|
|
825
|
+
"""Query SNS timeline posts from SnsTimeLine table."""
|
|
826
|
+
# Check if SnsTimeLine exists, fall back to SnsTopItem_1
|
|
827
|
+
tables = [r[0] for r in cursor.execute(
|
|
828
|
+
"SELECT name FROM sqlite_master WHERE type='table';").fetchall()]
|
|
829
|
+
|
|
830
|
+
table = 'SnsTimeLine' if 'SnsTimeLine' in tables else None
|
|
831
|
+
if not table:
|
|
832
|
+
table = 'SnsTopItem_1' if 'SnsTopItem_1' in tables else None
|
|
833
|
+
if not table:
|
|
834
|
+
return {'success': False, 'error': 'No SNS table found'}
|
|
835
|
+
|
|
836
|
+
conditions = []
|
|
837
|
+
params = []
|
|
838
|
+
|
|
839
|
+
if table == 'SnsTimeLine':
|
|
840
|
+
if usernames:
|
|
841
|
+
placeholders = ','.join(['?' for _ in usernames])
|
|
842
|
+
conditions.append(f'user_name IN ({placeholders})')
|
|
843
|
+
params.extend(usernames)
|
|
844
|
+
if keyword:
|
|
845
|
+
conditions.append('content LIKE ?')
|
|
846
|
+
params.append(f'%{keyword}%')
|
|
847
|
+
if start_time:
|
|
848
|
+
conditions.append("CAST(substr(content, instr(content, '<createTime>') + 12, 10) AS INTEGER) >= ?")
|
|
849
|
+
params.append(start_time)
|
|
850
|
+
if end_time:
|
|
851
|
+
conditions.append("CAST(substr(content, instr(content, '<createTime>') + 12, 10) AS INTEGER) <= ?")
|
|
852
|
+
params.append(end_time)
|
|
853
|
+
elif table == 'SnsTopItem_1':
|
|
854
|
+
uname_col = 'username' if 'username' in [r[1] for r in cursor.execute(f'PRAGMA table_info([{table}]);').fetchall()] else 'user_name'
|
|
855
|
+
if usernames:
|
|
856
|
+
placeholders = ','.join(['?' for _ in usernames])
|
|
857
|
+
conditions.append(f'{uname_col} IN ({placeholders})')
|
|
858
|
+
params.extend(usernames)
|
|
859
|
+
if keyword:
|
|
860
|
+
conditions.append('summary LIKE ?')
|
|
861
|
+
params.append(f'%{keyword}%')
|
|
862
|
+
if start_time:
|
|
863
|
+
conditions.append('create_time >= ?')
|
|
864
|
+
params.append(start_time)
|
|
865
|
+
if end_time:
|
|
866
|
+
conditions.append('create_time <= ?')
|
|
867
|
+
params.append(end_time)
|
|
868
|
+
|
|
869
|
+
where = ' AND '.join(conditions) if conditions else '1=1'
|
|
870
|
+
order = 'tid DESC' if table == 'SnsTimeLine' else 'create_time DESC'
|
|
871
|
+
|
|
872
|
+
try:
|
|
873
|
+
cols = [r[1] for r in cursor.execute(f'PRAGMA table_info([{table}]);').fetchall()]
|
|
874
|
+
# Only select known text columns to avoid binary decode errors
|
|
875
|
+
text_cols = [c for c in cols if c in ('tid', 'user_name', 'content', 'username', 'summary',
|
|
876
|
+
'create_time', 'last_read_time', 'is_read',
|
|
877
|
+
'from_username', 'from_nickname', 'to_username',
|
|
878
|
+
'to_nickname', 'comment_id', 'feed_id',
|
|
879
|
+
'createTime', 'userName')]
|
|
880
|
+
if not text_cols:
|
|
881
|
+
text_cols = ['*']
|
|
882
|
+
select_str = ', '.join(text_cols)
|
|
883
|
+
rows = cursor.execute(
|
|
884
|
+
f'SELECT {select_str} FROM [{table}] WHERE {where} ORDER BY {order} LIMIT ? OFFSET ?',
|
|
885
|
+
params + [limit, offset]
|
|
886
|
+
).fetchall()
|
|
887
|
+
|
|
888
|
+
timeline = []
|
|
889
|
+
|
|
890
|
+
for row in rows:
|
|
891
|
+
item = dict(zip(text_cols, row))
|
|
892
|
+
|
|
893
|
+
if table == 'SnsTimeLine':
|
|
894
|
+
# Parse XML content
|
|
895
|
+
content_str = item.get('content', '') or ''
|
|
896
|
+
parsed = parse_sns_content(content_str)
|
|
897
|
+
create_time = parsed['create_time'] or 0
|
|
898
|
+
username = parsed['username'] or item.get('user_name', '')
|
|
899
|
+
text = parsed['content'] or ''
|
|
900
|
+
media_count = parsed['media_count']
|
|
901
|
+
else:
|
|
902
|
+
create_time = item.get('create_time', 0) or 0
|
|
903
|
+
username = item.get('username', '') or item.get('user_name', '')
|
|
904
|
+
text = item.get('summary', '') or ''
|
|
905
|
+
media_count = 0
|
|
906
|
+
|
|
907
|
+
timeline.append({
|
|
908
|
+
'create_time': create_time,
|
|
909
|
+
'username': username,
|
|
910
|
+
'content': text,
|
|
911
|
+
'media_count': media_count,
|
|
912
|
+
'table': table,
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
return {'success': True, 'timeline': timeline, 'table': table}
|
|
916
|
+
except Exception as e:
|
|
917
|
+
return {'success': False, 'error': str(e)}
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def get_sns_usernames(cursor):
|
|
921
|
+
"""Get unique usernames from SnsTopItem_1."""
|
|
922
|
+
tables = [r[0] for r in cursor.execute(
|
|
923
|
+
"SELECT name FROM sqlite_master WHERE type='table';").fetchall()]
|
|
924
|
+
|
|
925
|
+
# Prefer SnsTopItem_1 for user listing (larger)
|
|
926
|
+
table = 'SnsTopItem_1' if 'SnsTopItem_1' in tables else ('SnsTimeLine' if 'SnsTimeLine' in tables else None)
|
|
927
|
+
if not table:
|
|
928
|
+
return {'success': False, 'error': 'No SNS table found'}
|
|
929
|
+
|
|
930
|
+
# Discover username column
|
|
931
|
+
cols = [r[1] for r in cursor.execute(f'PRAGMA table_info([{table}]);').fetchall()]
|
|
932
|
+
uname_col = 'username' if 'username' in cols else 'user_name'
|
|
933
|
+
|
|
934
|
+
try:
|
|
935
|
+
rows = cursor.execute(
|
|
936
|
+
f'SELECT [{uname_col}], COUNT(*) as cnt FROM [{table}] GROUP BY [{uname_col}] ORDER BY cnt DESC'
|
|
937
|
+
).fetchall()
|
|
938
|
+
usernames = [r[0] for r in rows if r[0]]
|
|
939
|
+
counts = {r[0]: r[1] for r in rows if r[0]}
|
|
940
|
+
return {'success': True, 'usernames': usernames, 'counts': counts}
|
|
941
|
+
except Exception as e:
|
|
942
|
+
return {'success': False, 'error': str(e)}
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def get_sns_stats(cursor, my_wxid=None):
|
|
946
|
+
"""Get SNS statistics from SnsTopItem_1."""
|
|
947
|
+
tables = [r[0] for r in cursor.execute(
|
|
948
|
+
"SELECT name FROM sqlite_master WHERE type='table';").fetchall()]
|
|
949
|
+
|
|
950
|
+
table = 'SnsTopItem_1' if 'SnsTopItem_1' in tables else ('SnsTimeLine' if 'SnsTimeLine' in tables else None)
|
|
951
|
+
if not table:
|
|
952
|
+
return {'success': False, 'error': 'No SNS table found'}
|
|
953
|
+
|
|
954
|
+
try:
|
|
955
|
+
total = cursor.execute(f'SELECT COUNT(*) FROM [{table}]').fetchone()[0]
|
|
956
|
+
|
|
957
|
+
cols = [r[1] for r in cursor.execute(f'PRAGMA table_info([{table}]);').fetchall()]
|
|
958
|
+
uname_col = 'username' if 'username' in cols else 'user_name'
|
|
959
|
+
|
|
960
|
+
total_friends = 0
|
|
961
|
+
if uname_col:
|
|
962
|
+
total_friends = cursor.execute(
|
|
963
|
+
f'SELECT COUNT(DISTINCT [{uname_col}]) FROM [{table}] WHERE [{uname_col}] IS NOT NULL'
|
|
964
|
+
).fetchone()[0]
|
|
965
|
+
|
|
966
|
+
my_posts = None
|
|
967
|
+
if my_wxid and uname_col:
|
|
968
|
+
my_posts = cursor.execute(
|
|
969
|
+
f'SELECT COUNT(*) FROM [{table}] WHERE [{uname_col}] = ?',
|
|
970
|
+
(my_wxid,)).fetchone()[0]
|
|
971
|
+
|
|
972
|
+
return {
|
|
973
|
+
'success': True,
|
|
974
|
+
'data': {
|
|
975
|
+
'totalPosts': total,
|
|
976
|
+
'totalFriends': total_friends,
|
|
977
|
+
'myPosts': my_posts,
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
except Exception as e:
|
|
981
|
+
return {'success': False, 'error': str(e)}
|
|
982
|
+
|
|
983
|
+
|
|
442
984
|
# ========== Main CLI ==========
|
|
443
985
|
|
|
444
986
|
def main():
|
|
@@ -449,57 +991,112 @@ def main():
|
|
|
449
991
|
# scan command
|
|
450
992
|
scan_parser = sub.add_parser('scan', help='Scan memory for keys and match NT databases')
|
|
451
993
|
scan_parser.add_argument('--json', action='store_true', help='Output as JSON')
|
|
994
|
+
scan_parser.add_argument('--root', default=os.environ.get('WEFLOW_SCAN_ROOT'), help='xwechat_files root directory override')
|
|
452
995
|
|
|
453
996
|
# sessions command
|
|
454
997
|
sessions_parser = sub.add_parser('sessions', help='List chat sessions')
|
|
455
|
-
sessions_parser.add_argument('--db', required=
|
|
456
|
-
sessions_parser.add_argument('--key', required=
|
|
457
|
-
sessions_parser.add_argument('--salt', required=
|
|
458
|
-
sessions_parser.add_argument('--keyword', help='Filter by keyword')
|
|
459
|
-
sessions_parser.add_argument('--contact-db', help='Path to contact.db for display names')
|
|
460
|
-
sessions_parser.add_argument('--contact-key', help='Contact DB key hex (64 chars)')
|
|
461
|
-
sessions_parser.add_argument('--contact-salt', help='Contact DB salt hex (32 chars)')
|
|
998
|
+
sessions_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to NT database')
|
|
999
|
+
sessions_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
|
|
1000
|
+
sessions_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
|
|
1001
|
+
sessions_parser.add_argument('--keyword', default=os.environ.get('WEFLOW_QUERY_KEYWORD'), help='Filter by keyword')
|
|
1002
|
+
sessions_parser.add_argument('--contact-db', default=os.environ.get('WEFLOW_CONTACT_DB'), help='Path to contact.db for display names')
|
|
1003
|
+
sessions_parser.add_argument('--contact-key', default=os.environ.get('WEFLOW_CONTACT_KEY'), help='Contact DB key hex (64 chars)')
|
|
1004
|
+
sessions_parser.add_argument('--contact-salt', default=os.environ.get('WEFLOW_CONTACT_SALT'), help='Contact DB salt hex (32 chars)')
|
|
462
1005
|
|
|
463
1006
|
# messages command
|
|
464
1007
|
msg_parser = sub.add_parser('messages', help='Get messages')
|
|
465
|
-
msg_parser.add_argument('--db', required=
|
|
466
|
-
msg_parser.add_argument('--key', required=
|
|
467
|
-
msg_parser.add_argument('--salt', required=
|
|
468
|
-
msg_parser.add_argument('--talker', required=
|
|
1008
|
+
msg_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to NT database')
|
|
1009
|
+
msg_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
|
|
1010
|
+
msg_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
|
|
1011
|
+
msg_parser.add_argument('--talker', default=os.environ.get('WEFLOW_TALKER'), required=not os.environ.get('WEFLOW_TALKER'), help='Talker username')
|
|
469
1012
|
msg_parser.add_argument('--limit', type=int, default=100)
|
|
470
1013
|
msg_parser.add_argument('--offset', type=int, default=0)
|
|
471
|
-
msg_parser.add_argument('--contact-db', help='Path to contact.db for sender names')
|
|
472
|
-
msg_parser.add_argument('--contact-key', help='Contact DB key hex (64 chars)')
|
|
473
|
-
msg_parser.add_argument('--contact-salt', help='Contact DB salt hex (32 chars)')
|
|
474
|
-
msg_parser.add_argument('--own-wxid', help='Account owner wxid (for self-message detection)')
|
|
1014
|
+
msg_parser.add_argument('--contact-db', default=os.environ.get('WEFLOW_CONTACT_DB'), help='Path to contact.db for sender names')
|
|
1015
|
+
msg_parser.add_argument('--contact-key', default=os.environ.get('WEFLOW_CONTACT_KEY'), help='Contact DB key hex (64 chars)')
|
|
1016
|
+
msg_parser.add_argument('--contact-salt', default=os.environ.get('WEFLOW_CONTACT_SALT'), help='Contact DB salt hex (32 chars)')
|
|
1017
|
+
msg_parser.add_argument('--own-wxid', default=os.environ.get('WEFLOW_OWN_WXID'), help='Account owner wxid (for self-message detection)')
|
|
475
1018
|
|
|
476
1019
|
# contacts command
|
|
477
1020
|
contacts_parser = sub.add_parser('contacts', help='List contacts')
|
|
478
|
-
contacts_parser.add_argument('--db', required=
|
|
479
|
-
contacts_parser.add_argument('--key', required=
|
|
480
|
-
contacts_parser.add_argument('--salt', required=
|
|
481
|
-
contacts_parser.add_argument('--keyword', help='Filter by keyword')
|
|
1021
|
+
contacts_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to NT database')
|
|
1022
|
+
contacts_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
|
|
1023
|
+
contacts_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
|
|
1024
|
+
contacts_parser.add_argument('--keyword', default=os.environ.get('WEFLOW_QUERY_KEYWORD'), help='Filter by keyword')
|
|
482
1025
|
contacts_parser.add_argument('--limit', type=int, default=200)
|
|
483
|
-
contacts_parser.add_argument('--contact-db', help='Path to contact.db for display names')
|
|
484
|
-
contacts_parser.add_argument('--contact-key', help='Contact DB key hex (64 chars)')
|
|
485
|
-
contacts_parser.add_argument('--contact-salt', help='Contact DB salt hex (32 chars)')
|
|
1026
|
+
contacts_parser.add_argument('--contact-db', default=os.environ.get('WEFLOW_CONTACT_DB'), help='Path to contact.db for display names')
|
|
1027
|
+
contacts_parser.add_argument('--contact-key', default=os.environ.get('WEFLOW_CONTACT_KEY'), help='Contact DB key hex (64 chars)')
|
|
1028
|
+
contacts_parser.add_argument('--contact-salt', default=os.environ.get('WEFLOW_CONTACT_SALT'), help='Contact DB salt hex (32 chars)')
|
|
1029
|
+
|
|
1030
|
+
# sns-timeline command
|
|
1031
|
+
sns_tl_parser = sub.add_parser('sns-timeline', help='Get SNS/Moments timeline')
|
|
1032
|
+
sns_tl_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to sns.db')
|
|
1033
|
+
sns_tl_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
|
|
1034
|
+
sns_tl_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
|
|
1035
|
+
sns_tl_parser.add_argument('--limit', type=int, default=20)
|
|
1036
|
+
sns_tl_parser.add_argument('--offset', type=int, default=0)
|
|
1037
|
+
sns_tl_parser.add_argument('--usernames', default=os.environ.get('WEFLOW_QUERY_USERNAMES'), help='JSON array of usernames to filter')
|
|
1038
|
+
sns_tl_parser.add_argument('--keyword', default=os.environ.get('WEFLOW_QUERY_KEYWORD'), help='Search keyword')
|
|
1039
|
+
sns_tl_parser.add_argument('--start-time', type=int, help='Start timestamp')
|
|
1040
|
+
sns_tl_parser.add_argument('--end-time', type=int, help='End timestamp')
|
|
1041
|
+
|
|
1042
|
+
# sns-usernames command
|
|
1043
|
+
sns_un_parser = sub.add_parser('sns-usernames', help='List usernames with SNS posts')
|
|
1044
|
+
sns_un_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to sns.db')
|
|
1045
|
+
sns_un_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
|
|
1046
|
+
sns_un_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
|
|
1047
|
+
|
|
1048
|
+
# sns-stats command
|
|
1049
|
+
sns_stats_parser = sub.add_parser('sns-stats', help='SNS statistics')
|
|
1050
|
+
sns_stats_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to sns.db')
|
|
1051
|
+
sns_stats_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
|
|
1052
|
+
sns_stats_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
|
|
1053
|
+
sns_stats_parser.add_argument('--my-wxid', default=os.environ.get('WEFLOW_OWN_WXID'), help='Account owner wxid for my-posts count')
|
|
1054
|
+
|
|
1055
|
+
# fav-schema command
|
|
1056
|
+
fav_schema_parser = sub.add_parser('fav-schema', help='Dump favorite.db schema (key verification)')
|
|
1057
|
+
fav_schema_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to favorite.db')
|
|
1058
|
+
fav_schema_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
|
|
1059
|
+
|
|
1060
|
+
verify_parser = sub.add_parser('verify', help='Verify a key+salt can open a database')
|
|
1061
|
+
verify_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to NT database')
|
|
1062
|
+
verify_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
|
|
1063
|
+
verify_parser.add_argument('--salt', default=os.environ.get('WEFLOW_NT_SALT'), required=not os.environ.get('WEFLOW_NT_SALT'), help='Salt hex (32 chars)')
|
|
1064
|
+
|
|
1065
|
+
# fav-list command
|
|
1066
|
+
fav_list_parser = sub.add_parser('fav-list', help='List favorite items')
|
|
1067
|
+
fav_list_parser.add_argument('--db', default=os.environ.get('WEFLOW_DB_PATH'), required=not os.environ.get('WEFLOW_DB_PATH'), help='Path to favorite.db')
|
|
1068
|
+
fav_list_parser.add_argument('--key', default=os.environ.get('WEFLOW_NT_KEY'), required=not os.environ.get('WEFLOW_NT_KEY'), help='Key hex (64 chars)')
|
|
1069
|
+
fav_list_parser.add_argument('--limit', type=int, default=100)
|
|
1070
|
+
fav_list_parser.add_argument('--offset', type=int, default=0)
|
|
1071
|
+
fav_list_parser.add_argument('--keyword', default=os.environ.get('WEFLOW_QUERY_KEYWORD'), help='Search keyword')
|
|
1072
|
+
fav_list_parser.add_argument('--type', type=int, dest='fav_type',
|
|
1073
|
+
help='Filter by type: 1=text 2=image 4=video 5=article 14=chatrecord')
|
|
486
1074
|
|
|
487
1075
|
args = parser.parse_args()
|
|
488
1076
|
|
|
489
1077
|
if args.command == 'scan':
|
|
490
1078
|
pid = find_weixin_pid()
|
|
491
1079
|
if not pid:
|
|
492
|
-
|
|
1080
|
+
if IS_WINDOWS:
|
|
1081
|
+
print(json.dumps({"error": "Weixin.exe 未运行"}))
|
|
1082
|
+
else:
|
|
1083
|
+
print(json.dumps({"error": "未检测到 Linux 微信进程,请确认微信已启动并登录"}))
|
|
493
1084
|
return
|
|
494
1085
|
|
|
495
1086
|
if not args.json:
|
|
496
1087
|
print(f"扫描进程 PID {pid}...")
|
|
497
1088
|
|
|
498
|
-
keys = scan_memory_keys(pid)
|
|
1089
|
+
keys, scan_err = scan_memory_keys(pid)
|
|
1090
|
+
if scan_err == 'permission':
|
|
1091
|
+
print(json.dumps({"error": "PERMISSION_DENIED: 读取微信进程内存需要 root 或 CAP_SYS_PTRACE 权限。可使用 sudo 运行,或执行: sudo setcap cap_sys_ptrace=ep $(which python3)"}))
|
|
1092
|
+
return
|
|
1093
|
+
if scan_err == 'gone':
|
|
1094
|
+
print(json.dumps({"error": "微信进程已退出,请重试"}))
|
|
1095
|
+
return
|
|
499
1096
|
if not args.json:
|
|
500
1097
|
print(f"找到 {len(keys)} 个密钥")
|
|
501
1098
|
|
|
502
|
-
databases = find_nt_databases()
|
|
1099
|
+
databases = find_nt_databases(getattr(args, 'root', None))
|
|
503
1100
|
if not args.json:
|
|
504
1101
|
print(f"找到 {len(databases)} 个 NT 数据库")
|
|
505
1102
|
|
|
@@ -510,6 +1107,20 @@ def main():
|
|
|
510
1107
|
print(f" {db['name']} ({db['size']/1024/1024:.1f}MB) key={db['key'][:16]}... salt={db['salt'][:16]}...")
|
|
511
1108
|
else:
|
|
512
1109
|
print(json.dumps({"keys": keys, "databases": databases, "matched": matched}))
|
|
1110
|
+
return
|
|
1111
|
+
|
|
1112
|
+
if args.command == 'verify':
|
|
1113
|
+
# sqlcipher 打开+读 sqlite_master 才触发解密, 错误密钥在此报 "file is not a database"
|
|
1114
|
+
try:
|
|
1115
|
+
conn, _ = connect_nt_db(args.db, args.key, args.salt)
|
|
1116
|
+
n = conn.execute(
|
|
1117
|
+
"SELECT count(*) FROM sqlite_master WHERE type='table'"
|
|
1118
|
+
).fetchone()[0]
|
|
1119
|
+
conn.close()
|
|
1120
|
+
print(json.dumps({"success": n > 0, "tables": n}, ensure_ascii=True))
|
|
1121
|
+
except Exception as e:
|
|
1122
|
+
print(json.dumps({"success": False, "error": str(e).split('\n')[0][:200]}, ensure_ascii=True))
|
|
1123
|
+
return
|
|
513
1124
|
|
|
514
1125
|
# Build contact name map once if contact db provided
|
|
515
1126
|
contact_name_map = {}
|
|
@@ -554,6 +1165,40 @@ def main():
|
|
|
554
1165
|
print(json.dumps(result, ensure_ascii=True))
|
|
555
1166
|
conn.close()
|
|
556
1167
|
|
|
1168
|
+
elif args.command == 'sns-timeline':
|
|
1169
|
+
conn, _ = connect_nt_db(args.db, args.key, args.salt)
|
|
1170
|
+
usernames = None
|
|
1171
|
+
if args.usernames:
|
|
1172
|
+
try:
|
|
1173
|
+
usernames = json.loads(args.usernames)
|
|
1174
|
+
except: pass
|
|
1175
|
+
result = get_sns_timeline(conn.cursor(), args.limit, args.offset,
|
|
1176
|
+
usernames, args.keyword,
|
|
1177
|
+
args.start_time, args.end_time)
|
|
1178
|
+
print(json.dumps(result, ensure_ascii=True, default=str))
|
|
1179
|
+
conn.close()
|
|
1180
|
+
|
|
1181
|
+
elif args.command == 'sns-usernames':
|
|
1182
|
+
conn, _ = connect_nt_db(args.db, args.key, args.salt)
|
|
1183
|
+
result = get_sns_usernames(conn.cursor())
|
|
1184
|
+
print(json.dumps(result, ensure_ascii=True, default=str))
|
|
1185
|
+
conn.close()
|
|
1186
|
+
|
|
1187
|
+
elif args.command == 'sns-stats':
|
|
1188
|
+
conn, _ = connect_nt_db(args.db, args.key, args.salt)
|
|
1189
|
+
result = get_sns_stats(conn.cursor(), args.my_wxid)
|
|
1190
|
+
print(json.dumps(result, ensure_ascii=True, default=str))
|
|
1191
|
+
conn.close()
|
|
1192
|
+
|
|
1193
|
+
elif args.command == 'fav-schema':
|
|
1194
|
+
result = get_fav_schema(args.db, args.key)
|
|
1195
|
+
print(json.dumps(result, ensure_ascii=True))
|
|
1196
|
+
|
|
1197
|
+
elif args.command == 'fav-list':
|
|
1198
|
+
result = get_favorites(args.db, args.key, args.limit, args.offset,
|
|
1199
|
+
args.keyword, getattr(args, 'fav_type', None))
|
|
1200
|
+
print(json.dumps(result, ensure_ascii=True, default=str))
|
|
1201
|
+
|
|
557
1202
|
else:
|
|
558
1203
|
parser.print_help()
|
|
559
1204
|
|