From 051ecf1b93507079f843d3e82a372fa39f78b069 Mon Sep 17 00:00:00 2001 From: xaoyaoo Date: Tue, 16 Jan 2024 23:24:16 +0800 Subject: [PATCH] =?UTF-8?q?=E8=81=8A=E5=A4=A9=E8=AE=B0=E5=BD=95=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=B7=BB=E5=8A=A0=E8=87=AA=E5=AE=9A=E4=B9=89=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=B7=AF=E5=BE=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pywxdump/api/__init__.py | 1 + pywxdump/api/api.py | 135 ++++++++++++++----- pywxdump/api/rjson.py | 1 + pywxdump/api/utils.py | 30 +++++ pywxdump/cli.py | 227 ++++++++++++++------------------ pywxdump/wx_info/get_wx_info.py | 9 ++ 6 files changed, 245 insertions(+), 158 deletions(-) create mode 100644 pywxdump/api/utils.py diff --git a/pywxdump/api/__init__.py b/pywxdump/api/__init__.py index 490e516..cd594d9 100644 --- a/pywxdump/api/__init__.py +++ b/pywxdump/api/__init__.py @@ -6,6 +6,7 @@ # Date: 2023/12/14 # ------------------------------------------------------------------------------- from .api import api +from .utils import read_session, save_session if __name__ == '__main__': pass diff --git a/pywxdump/api/api.py b/pywxdump/api/api.py index 9555688..f0e9446 100644 --- a/pywxdump/api/api.py +++ b/pywxdump/api/api.py @@ -6,11 +6,14 @@ # Date: 2024/01/02 # ------------------------------------------------------------------------------- import base64 +import logging import os +import time -from flask import Flask, request, render_template, g, Blueprint, send_file, make_response, send_from_directory -from pywxdump import analyzer, read_img_dat, read_audio +from flask import Flask, request, render_template, g, Blueprint, send_file, make_response, session +from pywxdump import analyzer, read_img_dat, read_audio, get_wechat_db from pywxdump.api.rjson import ReJson, RqJson +from pywxdump.api.utils import read_session, save_session from pywxdump import read_info, VERSION_LIST, batch_decrypt, BiasAddr, merge_db import pywxdump @@ -26,21 +29,85 @@ def init(): 初始化 :return: """ - # g.msg_path = path - # g.micro_path = path - # g.media_path = path - # g.wx_path = r"C:\Users\xaoyo\Documents\Tencent\WeChat Files\wxid_vzzcn5fevion22" - # g.my_wxid = "wxid_vzzcn5fevion22" + try: + msg_path = request.json.get("msg_path", "").strip() + micro_path = request.json.get("micro_path", "").strip() + media_path = request.json.get("media_path", "").strip() + wx_path = request.json.get("wx_path", "").strip() + key = request.json.get("key", "").strip() + my_wxid = request.json.get("my_wxid", "").strip() + + if key: # 如果key不为空,表示是解密模式 + if not wx_path: + return ReJson(1002) + if not os.path.exists(wx_path): + return ReJson(1001) + # 解密 + WxDbPath = get_wechat_db('all', None, wxid=my_wxid, is_logging=False) # 获取微信数据库路径 + if isinstance(WxDbPath, str): # 如果返回的是字符串,则表示出错 + print(WxDbPath) + return ReJson(4007) + wxdbpaths = [path for user_dir in WxDbPath.values() for paths in user_dir.values() for path in paths] + if len(wxdbpaths) == 0: + print("[-] 未获取到数据库路径") + return ReJson(4007) + + wxdbpaths = [i for i in wxdbpaths if "MicroMsg" in i or "MediaMSG" in i or r"Multi\MSG" in i] # 过滤掉无需解密的数据库 + decrypted_path = os.path.join(g.tmp_path, "decrypted") + + # 判断out_path是否为空目录 + if os.path.exists(decrypted_path) and os.listdir(decrypted_path): + isdel = "y" + if isdel.lower() == 'y' or isdel.lower() == 'yes': + for root, dirs, files in os.walk(decrypted_path, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + + out_path = os.path.join(decrypted_path, my_wxid) if my_wxid else decrypted_path + if not os.path.exists(out_path): + os.makedirs(out_path) + + # 调用 decrypt 函数,并传入参数 # 解密 + code, ret = batch_decrypt(key, wxdbpaths, out_path, False) + if not code: + return ReJson(4007, msg=ret) + + out_dbs = [] + for code1, ret1 in ret: + if code1: + out_dbs.append(ret1[1]) + + parpare_merge_db_path = [i for i in out_dbs if "de_MicroMsg" in i or "de_MediaMSG" in i or "de_MSG" in i] + # 合并所有的数据库 + logging.info("开始合并数据库") + merge_save_path = merge_db(parpare_merge_db_path, os.path.join(out_path, "merge_all.db")) + time.sleep(1) + + save_session(g.sf, "msg_path", merge_save_path) + save_session(g.sf, "micro_path", merge_save_path) + save_session(g.sf, "media_path", merge_save_path) + save_session(g.sf, "wx_path", wx_path) + save_session(g.sf, "key", key) + save_session(g.sf, "my_wxid", my_wxid) + + rdata = { + "msg_path": merge_save_path, + "micro_path": merge_save_path, + "media_path": merge_save_path, + "wx_path": wx_path, + "key": key, + "my_wxid": my_wxid, + "is_init": True, + } + return ReJson(0, rdata) + + else: + return ReJson(1002) + except Exception as e: + return ReJson(9999, msg=str(e)) - rdata = { - "msg_path": "", - "micro_path": "", - "media_path": "", - "wx_path": "", - "my_wxid": "", - "is_init": False, - } - return ReJson(0, rdata) @api.route('/api/version', methods=["GET", 'POST']) def version(): @@ -50,6 +117,7 @@ def version(): """ return ReJson(0, pywxdump.__version__) + @api.route('/api/contact_list', methods=["GET", 'POST']) def contact_list(): """ @@ -61,12 +129,12 @@ def contact_list(): # 从header中读取micro_path micro_path = request.headers.get("micro_path") if not micro_path: - micro_path = g.micro_path + micro_path = read_session(g.sf, "micro_path") start = request.json.get("start") limit = request.json.get("limit") contact_list = analyzer.get_contact_list(micro_path) - g.user_list = contact_list + save_session(g.sf, "user_list", contact_list) if limit: contact_list = contact_list[int(start):int(start) + int(limit)] return ReJson(0, contact_list) @@ -85,7 +153,7 @@ def chat_count(): # 从header中读取micro_path msg_path = request.headers.get("msg_path") if not msg_path: - msg_path = g.msg_path + msg_path = read_session(g.sf, "msg_path") username = request.json.get("username", "") contact_list = analyzer.get_chat_count(msg_path, username) return ReJson(0, contact_list) @@ -105,9 +173,9 @@ def contact_count_list(): msg_path = request.headers.get("msg_path") micro_path = request.headers.get("micro_path") if not msg_path: - msg_path = g.msg_path + msg_path = read_session(g.sf, "msg_path") if not micro_path: - micro_path = g.micro_path + micro_path = read_session(g.sf, "micro_path") start = request.json.get("start") limit = request.json.get("limit") word = request.json.get("word", "") @@ -121,7 +189,7 @@ def contact_count_list(): # 降序 contact_list = sorted(contact_list, key=lambda x: x["chat_count"], reverse=True) - g.user_list = contact_list + save_session(g.sf, "user_list", contact_list) if word and word != "" and word != "undefined" and word != "null": contact_list = [contact for contact in contact_list if @@ -139,9 +207,9 @@ def get_msgs(): msg_path = request.headers.get("msg_path") micro_path = request.headers.get("micro_path") if not msg_path: - msg_path = g.msg_path + msg_path = read_session(g.sf, "msg_path") if not micro_path: - micro_path = g.micro_path + micro_path = read_session(g.sf, "micro_path") start = request.json.get("start") limit = request.json.get("limit") wxid = request.json.get("wxid") @@ -151,9 +219,10 @@ def get_msgs(): contact_list = analyzer.get_contact_list(micro_path) userlist = {} + my_wxid = read_session(g.sf, "my_wxid") if wxid.endswith("@chatroom"): # 群聊 - talkers = [msg["talker"] for msg in msg_list] + [wxid, g.my_wxid] + talkers = [msg["talker"] for msg in msg_list] + [wxid, my_wxid] talkers = list(set(talkers)) for user in contact_list: if user["username"] in talkers: @@ -161,12 +230,12 @@ def get_msgs(): else: # 单聊 for user in contact_list: - if user["username"] == wxid or user["username"] == g.my_wxid: + if user["username"] == wxid or user["username"] == my_wxid: userlist[user["username"]] = user if len(userlist) == 2: break - return ReJson(0, {"msg_list": msg_list, "user_list": userlist, "my_wxid": g.my_wxid}) + return ReJson(0, {"msg_list": msg_list, "user_list": userlist, "my_wxid": my_wxid}) @api.route('/api/img', methods=["GET", 'POST']) @@ -179,7 +248,8 @@ def get_img(): img_path = request.json.get("img_path", img_path) if not img_path: return ReJson(1002) - img_path_all = os.path.join(g.wx_path, img_path) + wx_path = read_session(g.sf, "wx_path") + img_path_all = os.path.join(wx_path, img_path) if os.path.exists(img_path_all): fomt, md5, out_bytes = read_img_dat(img_path_all) out_bytes = base64.b64encode(out_bytes).decode("utf-8") @@ -197,7 +267,8 @@ def get_audio(savePath): MsgSvrID = savePath.split("_")[-1].replace(".wav", "") if not savePath: return ReJson(1002) - wave_data = read_audio(MsgSvrID, is_wave=True, DB_PATH=g.media_path) + media_path = read_session(g.sf, "media_path") + wave_data = read_audio(MsgSvrID, is_wave=True, DB_PATH=media_path) if not wave_data: return ReJson(1001) # 判断savePath路径的文件夹是否存在 @@ -223,8 +294,8 @@ def export(): username = request.json.get("username") # 可选参数 - wx_path = request.json.get("wx_path", g.wx_path) - key = request.json.get("key", "") + wx_path = request.json.get("wx_path", read_session(g.sf, "wx_path")) + key = request.json.get("key", read_session(g.sf, "key")) if not export_type or not start_time or not end_time or not chat_type or not username: return ReJson(1002) @@ -250,7 +321,7 @@ def export(): outpath = os.path.join(outpath, "csv") if not os.path.exists(outpath): os.makedirs(outpath) - code, ret = analyzer.export_csv(username, outpath, g.msg_path) + code, ret = analyzer.export_csv(username, outpath, read_session(g.sf, "msg_path")) if code: return ReJson(0, ret) elif export_type == "json": diff --git a/pywxdump/api/rjson.py b/pywxdump/api/rjson.py index 11a3e87..cfc2555 100644 --- a/pywxdump/api/rjson.py +++ b/pywxdump/api/rjson.py @@ -25,6 +25,7 @@ def ReJson(code: int, body: [dict, list] = None, msg: str = None, error: str = N 4004: {'code': 4004, 'body': body, 'msg': "数据不存在!", "extra": extra}, 4005: {'code': 4005, 'body': body, 'msg': "数据库异常!", "extra": extra}, 4006: {'code': 4006, 'body': body, 'msg': "数据已存在!", "extra": extra}, + 4007: {'code': 4007, 'body': body, 'msg': "数据库解密异常!", "extra": extra}, 5002: {'code': 5002, 'body': body, 'msg': "服务器错误!", "extra": extra}, 9999: {'code': 9999, 'body': body, 'msg': "未知错误!", "extra": extra}, } diff --git a/pywxdump/api/utils.py b/pywxdump/api/utils.py new file mode 100644 index 0000000..01a6f82 --- /dev/null +++ b/pywxdump/api/utils.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*-# +# ------------------------------------------------------------------------------- +# Name: utils.py +# Description: +# Author: xaoyaoo +# Date: 2024/01/16 +# ------------------------------------------------------------------------------- +import json + + +def read_session(session_file, arg): + with open(session_file, 'r') as f: + session = json.load(f) + return session.get(arg, "") + + +def save_session(session_file, arg, value): + try: + with open(session_file, 'r') as f: + session = json.load(f) + except: + session = {} + session[arg] = value + with open(session_file, 'w') as f: + json.dump(session, f, indent=4) + return True + + +if __name__ == '__main__': + pass diff --git a/pywxdump/cli.py b/pywxdump/cli.py index 7aba110..5138372 100644 --- a/pywxdump/cli.py +++ b/pywxdump/cli.py @@ -210,80 +210,15 @@ class MainShowChatRecords(): args.micro_path = merge_path args.media_path = merge_path - try: - from flask import Flask, request, jsonify, render_template, g - import logging - except Exception as e: - print(e) - print("[-] 请安装flask( pip install flask )") - return + online = args.online if not os.path.exists(args.msg_path) or not os.path.exists(args.micro_path) or not os.path.exists( args.media_path): print("[-] 输入数据库路径不存在") return - from flask_cors import CORS - from pywxdump.api import api - - app = Flask(__name__, template_folder='./ui/web', static_folder='./ui/web/assets/', static_url_path='/assets/') - - app.logger.setLevel(logging.ERROR) - - CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True) # 允许所有域名跨域 - - @app.before_request - def before_request(): - g.msg_path = args.msg_path - g.micro_path = args.micro_path - g.media_path = args.media_path - g.wx_path = args.wx_path - # g.key = args.key - g.my_wxid = args.my_wxid - g.tmp_path = os.path.join(os.getcwd(), "wxdump_tmp") # 临时文件夹,用于存放图片等 - g.user_list = [] - - app.register_blueprint(api) - - try: - # 自动打开浏览器 - url = "http://127.0.0.1:5000/" - # 根据操作系统使用不同的命令打开默认浏览器 - if sys.platform.startswith('darwin'): # macOS - subprocess.call(['open', url]) - elif sys.platform.startswith('win'): # Windows - subprocess.call(['start', url], shell=True) - elif sys.platform.startswith('linux'): # Linux - subprocess.call(['xdg-open', url]) - else: - print("Unsupported platform, can't open browser automatically.") - except Exception as e: - pass - - def is_port_in_use(host, port): - import socket - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - try: - s.bind((host, port)) - except socket.error: - return True - return False - - online = args.online - # 检查端口是否被占用 - if online: - host = '0.0.0.0' - else: - host = "127.0.0.1" - - port = 5000 - if is_port_in_use(host, port): - print(f"Port {port} is already in use. Choose a different port.") - input("Press Enter to exit...") - else: - time.sleep(1) - print("[+] 请使用浏览器访问 http://127.0.0.1:5000/ 查看聊天记录") - app.run(host=host, port=port, debug=False) + start_falsk(msg_path=args.msg_path, micro_path=args.micro_path, media_path=args.media_path, + wx_path=args.wx_path, key="", my_wxid=args.my_wxid, online=online) class MainExportChatRecords(): @@ -455,6 +390,103 @@ class MainAll(): MainShowChatRecords().run(args) +def start_falsk(merge_path="", msg_path="", micro_path="", media_path="", wx_path="", key="", my_wxid="", port=5000, + online=False, debug=False): + """ + 启动flask + :param merge_path: 合并后的数据库路径 + :param msg_path: MSG.db 的路径 + :param micro_path: MicroMsg.db 的路径 + :param media_path: MediaMSG.db 的路径 + :param wx_path: 微信文件夹的路径(用于显示图片) + :param key: 密钥 + :param my_wxid: 微信账号(本人微信id) + :param port: 端口号 + :param online: 是否在线查看(局域网查看) + :param debug: 是否开启debug模式 + :return: + """ + tmp_path = os.path.join(os.getcwd(), "wxdump_tmp") # 临时文件夹,用于存放图片等 + if not os.path.exists(tmp_path): + os.makedirs(tmp_path) + print(f"[+] 创建临时文件夹:{tmp_path}") + + session_file = os.path.join(tmp_path, "session") # 用于存放各种基础信息 + + from flask import Flask, g + from flask_cors import CORS + from pywxdump.api import api, read_session, save_session + import logging + + if merge_path: + msg_path = merge_path + micro_path = merge_path + media_path = merge_path + + # 检查端口是否被占用 + if online: + host = '0.0.0.0' + else: + host = "127.0.0.1" + + app = Flask(__name__, template_folder='./ui/web', static_folder='./ui/web/assets/', static_url_path='/assets/') + + # 设置超时时间为 1000 秒 + app.config['TIMEOUT'] = 1000 + app.secret_key = 'secret_key' + + app.logger.setLevel(logging.ERROR) + + CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True) # 允许所有域名跨域 + + @app.before_request + def before_request(): + + g.tmp_path = tmp_path # 临时文件夹,用于存放图片等 + g.sf = session_file # 用于存放各种基础信息 + + if msg_path: save_session(session_file, "msg_path", msg_path) + if micro_path: save_session(session_file, "micro_path", micro_path) + if media_path: save_session(session_file, "media_path", media_path) + if wx_path: save_session(session_file, "wx_path", wx_path) + if key: save_session(session_file, "key", key) + if my_wxid: save_session(session_file, "my_wxid", my_wxid) + + app.register_blueprint(api) + + try: + # 自动打开浏览器 + url = f"http://127.0.0.1:{port}/" + # 根据操作系统使用不同的命令打开默认浏览器 + if sys.platform.startswith('darwin'): # macOS + subprocess.call(['open', url]) + elif sys.platform.startswith('win'): # Windows + subprocess.call(['start', url], shell=True) + elif sys.platform.startswith('linux'): # Linux + subprocess.call(['xdg-open', url]) + else: + print("Unsupported platform, can't open browser automatically.") + except Exception as e: + pass + + def is_port_in_use(host, port): + import socket + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.bind((host, port)) + except socket.error: + return True + return False + + if is_port_in_use(host, port): + print(f"Port {port} is already in use. Choose a different port.") + input("Press Enter to exit...") + else: + time.sleep(1) + print("[+] 请使用浏览器访问 http://127.0.0.1:5000/ 查看聊天记录") + app.run(host=host, port=port, debug=debug) + + class MainUi(): def init_parses(self, parser): self.mode = "ui" @@ -470,67 +502,10 @@ class MainUi(): print(f"[*] PyWxDump v{pywxdump.__version__}") # 从命令行参数获取值 online = args.online - # 检查端口是否被占用 - if online: - host = '0.0.0.0' - else: - host = "127.0.0.1" port = args.port debug = args.debug - from flask import Flask, g - import logging - from flask_cors import CORS - from pywxdump.api import api - - app = Flask(__name__, template_folder='./ui/web', static_folder='./ui/web/assets/', static_url_path='/assets/') - - app.logger.setLevel(logging.ERROR) - - CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True) # 允许所有域名跨域 - - @app.before_request - def before_request(): - g.msg_path = "" - g.media_path = "" - g.wx_path = "" - g.my_wxid = "" - g.tmp_path = os.path.join(os.getcwd(), "wxdump_tmp") # 临时文件夹,用于存放图片等 - g.user_list = [] - - app.register_blueprint(api) - - try: - # 自动打开浏览器 - url = f"http://127.0.0.1:{port}/" - # 根据操作系统使用不同的命令打开默认浏览器 - if sys.platform.startswith('darwin'): # macOS - subprocess.call(['open', url]) - elif sys.platform.startswith('win'): # Windows - subprocess.call(['start', url], shell=True) - elif sys.platform.startswith('linux'): # Linux - subprocess.call(['xdg-open', url]) - else: - print("Unsupported platform, can't open browser automatically.") - except Exception as e: - pass - - def is_port_in_use(host, port): - import socket - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - try: - s.bind((host, port)) - except socket.error: - return True - return False - - if is_port_in_use(host, port): - print(f"Port {port} is already in use. Choose a different port.") - input("Press Enter to exit...") - else: - time.sleep(1) - print("[+] 请使用浏览器访问 http://127.0.0.1:5000/ 查看聊天记录") - app.run(host=host, port=port, debug=debug) + start_falsk(port=port, online=online, debug=debug) class CustomArgumentParser(argparse.ArgumentParser): diff --git a/pywxdump/wx_info/get_wx_info.py b/pywxdump/wx_info/get_wx_info.py index 69c5e29..6f88f4f 100644 --- a/pywxdump/wx_info/get_wx_info.py +++ b/pywxdump/wx_info/get_wx_info.py @@ -225,6 +225,15 @@ def read_info(version_list: dict = None, is_logging: bool = False, save_path: st def get_wechat_db(require_list: Union[List[str], str] = "all", msg_dir: str = None, wxid: Union[List[str], str] = None, is_logging: bool = False): + r""" + 获取微信数据库路径 + :param require_list: 需要获取的数据库类型 + :param msg_dir: 微信数据库目录 eg: C:\Users\user\Documents\WeChat Files + :param wxid: 微信id + :param is_logging: 是否打印日志 + :return: + """ + if not msg_dir: msg_dir = get_info_filePath(wxid="all")