diff --git a/pywxdump/__init__.py b/pywxdump/__init__.py index 6188574..bb91779 100644 --- a/pywxdump/__init__.py +++ b/pywxdump/__init__.py @@ -5,15 +5,8 @@ # Author: xaoyaoo # Date: 2023/10/14 # ------------------------------------------------------------------------------- -# from .analyzer.db_parsing import read_img_dat, read_emoji, decompress_CompressContent, read_audio_buf, read_audio, \ -# parse_xml_string, read_BytesExtra -# from .ui import app_show_chat, get_user_list, export -from .wx_core import BiasAddr, get_wx_info, get_wx_db, batch_decrypt, decrypt, get_core_db -from .wx_core import merge_db, decrypt_merge, merge_real_time_db, all_merge_real_time_db -from .analyzer import DBPool -from .db import MsgHandler, MicroHandler, \ - MediaHandler, OpenIMContactHandler, FavoriteHandler, PublicMsgHandler, DBHandler -from .server import start_falsk +__version__ = "3.1.18" + import os, json try: @@ -24,7 +17,18 @@ except: WX_OFFS = {} WX_OFFS_PATH = None +from .wx_core import BiasAddr, get_wx_info, get_wx_db, batch_decrypt, decrypt, get_core_db +from .wx_core import merge_db, decrypt_merge, merge_real_time_db, all_merge_real_time_db +from .db import DBHandler, MsgHandler, MicroHandler, MediaHandler, OpenIMContactHandler, FavoriteHandler, \ + PublicMsgHandler +from .api import start_server, app +from .analyzer import DBPool + # PYWXDUMP_ROOT_PATH = os.path.dirname(__file__) # db_init = DBPool("DBPOOL_INIT") -__version__ = "3.1.18" + +__all__ = ["BiasAddr", "get_wx_info", "get_wx_db", "batch_decrypt", "decrypt", "get_core_db", + "merge_db", "decrypt_merge", "merge_real_time_db", "all_merge_real_time_db", + "MsgHandler", "MicroHandler", "MediaHandler", "OpenIMContactHandler", "FavoriteHandler", "PublicMsgHandler", + "DBHandler", "start_server", "WX_OFFS", "WX_OFFS_PATH", "__version__", "app"] diff --git a/pywxdump/api/__init__.py b/pywxdump/api/__init__.py index a734459..13f29a3 100644 --- a/pywxdump/api/__init__.py +++ b/pywxdump/api/__init__.py @@ -5,9 +5,132 @@ # Author: xaoyaoo # Date: 2023/12/14 # ------------------------------------------------------------------------------- + +import logging +import os +import subprocess +import sys +import time +import uvicorn + +from fastapi import FastAPI, Request, Path, Query +from fastapi.staticfiles import StaticFiles +from fastapi.exceptions import RequestValidationError +from starlette.middleware.cors import CORSMiddleware +from starlette.responses import RedirectResponse + +from .utils import gc, is_port_in_use, server_loger +from .rjson import ReJson from .remote_server import rs_api from .local_server import ls_api -from .utils import get_conf, set_conf -if __name__ == '__main__': - pass +from pywxdump import __version__ + +app = FastAPI(title="pywxdump", description="微信工具", version=__version__, + terms_of_service="https://github.com/xaoyaoo/pywxdump", + contact={"name": "xaoyaoo", "url": "https://github.com/xaoyaoo/pywxdump", "email": ""}, + license_info={"name": "MIT License", "url": "https://github.com/xaoyaoo/pywxdump/blob/main/LICENSE"}) + +# 跨域 +origins = [ + "http://localhost:5000", + "http://127.0.0.1:5000", + "http://localhost:8080", # 开发环境的客户端地址" + # "http://0.0.0.0:5000", + # "*" +] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, # 允许所有源 + allow_credentials=True, + allow_methods=["*"], # 允许所有方法 + allow_headers=["*"], # 允许所有头 +) + + +@app.exception_handler(RequestValidationError) +async def request_validation_exception_handler(request: Request, exc: RequestValidationError): + # print(request.body) + return ReJson(1002, {"detail": exc.errors()}) + + +@app.get("/") +@app.get("/index.html") +async def redirect(): + response = RedirectResponse(url="/s/index.html", status_code=307) + return response + + +# 路由挂载 +app.include_router(rs_api, prefix='/api/rs', tags=['远程api']) +app.include_router(ls_api, prefix='/api/ls', tags=['本地api']) + +web_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "ui", "web") +# 静态文件挂载 +app.mount("/s", StaticFiles(directory=web_path), name="static") + + +def start_server(port=5000, online=False, debug=False, isopenBrowser=True): + """ + 启动flask + :param port: 端口号 + :param online: 是否在线查看(局域网查看) + :param debug: 是否开启debug模式 + :param isopenBrowser: 是否自动打开浏览器 + :return: + """ + # 全局变量 + work_path = os.path.join(os.getcwd(), "wxdump_work") # 临时文件夹,用于存放图片等 + if not os.path.exists(work_path): + os.makedirs(work_path) + server_loger.info(f"[+] 创建临时文件夹:{work_path}") + print(f"[+] 创建临时文件夹:{work_path}") + conf_file = os.path.join(work_path, "conf_auto.json") # 用于存放各种基础信息 + auto_setting = "auto_setting" + env_file = os.path.join(work_path, ".env") # 用于存放环境变量 + # set 环境变量 + os.environ["PYWXDUMP_WORK_PATH"] = work_path + os.environ["PYWXDUMP_CONF_FILE"] = conf_file + os.environ["PYWXDUMP_AUTO_SETTING"] = auto_setting + + with open(env_file, "w", encoding="utf-8") as f: + f.write(f"PYWXDUMP_WORK_PATH = '{work_path}'\n") + f.write(f"PYWXDUMP_CONF_FILE = '{conf_file}'\n") + f.write(f"PYWXDUMP_AUTO_SETTING = '{auto_setting}'\n") + + # 检查端口是否被占用 + if online: + host = '0.0.0.0' + else: + host = "127.0.0.1" + + if is_port_in_use(host, port): + server_loger.error(f"Port {port} is already in use. Choose a different port.") + print(f"Port {port} is already in use. Choose a different port.") + input("Press Enter to exit...") + return # 退出程序 + if isopenBrowser: + 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: + server_loger.error(f"Unsupported platform, can't open browser automatically.", exc_info=True) + print("Unsupported platform, can't open browser automatically.") + except Exception as e: + server_loger.error(f"自动打开浏览器失败:{e}", exc_info=True) + + time.sleep(1) + server_loger.info(f"启动flask服务,host:port:{host}:{port}") + print("[+] 请使用浏览器访问 http://127.0.0.1:5000/ 查看聊天记录") + + uvicorn.run(app=app, host=host, port=port, reload=debug, log_level="info", workers=1, env_file=env_file) + + +__all__ = ["start_server", "app"] diff --git a/pywxdump/api/local_server.py b/pywxdump/api/local_server.py index 279ac13..2fdc909 100644 --- a/pywxdump/api/local_server.py +++ b/pywxdump/api/local_server.py @@ -5,58 +5,55 @@ # Author: xaoyaoo # Date: 2024/08/01 # ------------------------------------------------------------------------------- -import base64 -import json -import logging import os -import re import time import shutil import pythoncom -import pywxdump -from flask import Flask, request, render_template, g, Blueprint, send_file, make_response, session -from pywxdump import get_core_db, all_merge_real_time_db, get_wx_db -from pywxdump.api.rjson import ReJson, RqJson -from pywxdump.api.utils import get_conf, get_conf_wxids, set_conf, error9999, gen_base64, validate_title, \ - get_conf_local_wxid, ls_loger, random_str -from pywxdump import get_wx_info, WX_OFFS, batch_decrypt, BiasAddr, merge_db, decrypt_merge, merge_real_time_db -ls_api = Blueprint('ls_api', __name__, template_folder='../ui/web', static_folder='../ui/web/assets/', ) -ls_api.debug = False +from pydantic import BaseModel +from fastapi import APIRouter + +from pywxdump import all_merge_real_time_db, get_wx_db +from pywxdump import get_wx_info, batch_decrypt, BiasAddr, merge_db, decrypt_merge + +from .rjson import ReJson, RqJson +from .utils import error9999, ls_loger, random_str, gc + +ls_api = APIRouter() # 以下为初始化相关 ******************************************************************************************************* -@ls_api.route('/api/ls/init_last_local_wxid', methods=["GET", 'POST']) +@ls_api.api_route('/init_last_local_wxid', methods=["GET", 'POST']) @error9999 def init_last_local_wxid(): """ 初始化,包括key :return: """ - local_wxid = get_conf_local_wxid(g.caf) - local_wxid.remove(g.at) + local_wxid = gc.get_local_wxids() + local_wxid.remove(gc.at) if local_wxid: return ReJson(0, {"local_wxids": local_wxid}) return ReJson(0, {"local_wxids": []}) -@ls_api.route('/api/ls/init_last', methods=["GET", 'POST']) +@ls_api.api_route('/init_last', methods=["GET", 'POST']) @error9999 -def init_last(): +def init_last(my_wxid: str): """ 是否初始化 :return: """ - my_wxid = request.json.get("my_wxid", "") my_wxid = my_wxid.strip().strip("'").strip('"') if isinstance(my_wxid, str) else "" if not my_wxid: - my_wxid = get_conf(g.caf, "auto_setting", "last") + my_wxid = gc.get_conf(gc.at, "last") + if not my_wxid: return ReJson(1001, body="my_wxid is required") if my_wxid: - set_conf(g.caf, "auto_setting", "last", my_wxid) - merge_path = get_conf(g.caf, my_wxid, "merge_path") - wx_path = get_conf(g.caf, my_wxid, "wx_path") - key = get_conf(g.caf, my_wxid, "key") + gc.set_conf(gc.at, "last", my_wxid) + merge_path = gc.get_conf(my_wxid, "merge_path") + wx_path = gc.get_conf(my_wxid, "wx_path") + key = gc.get_conf(my_wxid, "key") rdata = { "merge_path": merge_path, "wx_path": wx_path, @@ -69,16 +66,23 @@ def init_last(): return ReJson(0, {"is_init": False, "my_wxid": ""}) -@ls_api.route('/api/ls/init_key', methods=["GET", 'POST']) +class InitKeyRequest(BaseModel): + wx_path: str + key: str + my_wxid: str + + +@ls_api.api_route('/init_key', methods=["GET", 'POST']) @error9999 -def init_key(): +def init_key(request: InitKeyRequest): """ - 初始化,包括key + 初始化key + :param request: :return: """ - wx_path = request.json.get("wx_path", "").strip().strip("'").strip('"') - key = request.json.get("key", "").strip().strip("'").strip('"') - my_wxid = request.json.get("my_wxid", "").strip().strip("'").strip('"') + wx_path = request.wx_path.strip().strip("'").strip('"') + key = request.key.strip().strip("'").strip('"') + my_wxid = request.my_wxid.strip().strip("'").strip('"') if not wx_path: return ReJson(1002, body=f"wx_path is required: {wx_path}") if not os.path.exists(wx_path): @@ -92,8 +96,10 @@ def init_key(): # if isinstance(db_config, dict) and db_config and os.path.exists(db_config.get("path")): # pmsg = DBHandler(db_config) # # pmsg.close_all_connection() + print(id(gc)) + print(wx_path, "\n", key, "\n", my_wxid, "\n", gc.work_path) - out_path = os.path.join(g.work_path, "decrypted", my_wxid) if my_wxid else os.path.join(g.work_path, "decrypted") + out_path = os.path.join(gc.work_path, "decrypted", my_wxid) if my_wxid else os.path.join(gc.work_path, "decrypted") # 检查文件夹中文件是否被占用 if os.path.exists(out_path): try: @@ -107,9 +113,9 @@ def init_key(): time.sleep(1) if code: # 移动merge_save_path到g.work_path/my_wxid - if not os.path.exists(os.path.join(g.work_path, my_wxid)): - os.makedirs(os.path.join(g.work_path, my_wxid)) - merge_save_path_new = os.path.join(g.work_path, my_wxid, "merge_all.db") + if not os.path.exists(os.path.join(gc.work_path, my_wxid)): + os.makedirs(os.path.join(gc.work_path, my_wxid)) + merge_save_path_new = os.path.join(gc.work_path, my_wxid, "merge_all.db") shutil.move(merge_save_path, str(merge_save_path_new)) # 删除out_path @@ -124,12 +130,13 @@ def init_key(): "type": "sqlite", "path": merge_save_path_new } - set_conf(g.caf, my_wxid, "db_config", db_config) - set_conf(g.caf, my_wxid, "merge_path", merge_save_path_new) - set_conf(g.caf, my_wxid, "wx_path", wx_path) - set_conf(g.caf, my_wxid, "key", key) - set_conf(g.caf, my_wxid, "my_wxid", my_wxid) - set_conf(g.caf, "auto_setting", "last", my_wxid) + gc.set_conf(my_wxid, "db_config", db_config) + gc.set_conf(my_wxid, "db_config", db_config) + gc.set_conf(my_wxid, "merge_path", merge_save_path_new) + gc.set_conf(my_wxid, "wx_path", wx_path) + gc.set_conf(my_wxid, "key", key) + gc.set_conf(my_wxid, "my_wxid", my_wxid) + gc.set_conf(gc.at, "last", my_wxid) rdata = { "merge_path": merge_save_path_new, "wx_path": wx_path, @@ -142,16 +149,22 @@ def init_key(): return ReJson(2001, body=merge_save_path) -@ls_api.route('/api/ls/init_nokey', methods=["GET", 'POST']) +class InitNoKeyRequest(BaseModel): + merge_path: str + wx_path: str + my_wxid: str + + +@ls_api.post('/init_nokey') @error9999 -def init_nokey(): +def init_nokey(request: InitNoKeyRequest): """ 初始化,包括key :return: """ - merge_path = request.json.get("merge_path", "").strip().strip("'").strip('"') - wx_path = request.json.get("wx_path", "").strip().strip("'").strip('"') - my_wxid = request.json.get("my_wxid", "").strip().strip("'").strip('"') + merge_path = request.merge_path.strip().strip("'").strip('"') + wx_path = request.wx_path.strip().strip("'").strip('"') + my_wxid = request.my_wxid.strip().strip("'").strip('"') if not wx_path: return ReJson(1002, body=f"wx_path is required: {wx_path}") @@ -162,18 +175,18 @@ def init_nokey(): if not my_wxid: return ReJson(1002, body=f"my_wxid is required: {my_wxid}") - key = get_conf(g.caf, my_wxid, "key") + key = gc.get_conf(my_wxid, "key") db_config = { "key": random_str(16), "type": "sqlite", "path": merge_path } - set_conf(g.caf, my_wxid, "db_config", db_config) - set_conf(g.caf, my_wxid, "merge_path", merge_path) - set_conf(g.caf, my_wxid, "wx_path", wx_path) - set_conf(g.caf, my_wxid, "key", key) - set_conf(g.caf, my_wxid, "my_wxid", my_wxid) - set_conf(g.caf, g.at, "last", my_wxid) + gc.set_conf(my_wxid, "db_config", db_config) + gc.set_conf(my_wxid, "merge_path", merge_path) + gc.set_conf(my_wxid, "wx_path", wx_path) + gc.set_conf(my_wxid, "key", key) + gc.set_conf(my_wxid, "my_wxid", my_wxid) + gc.set_conf(gc.at, "last", my_wxid) rdata = { "merge_path": merge_path, "wx_path": wx_path, @@ -187,24 +200,24 @@ def init_nokey(): # END 以上为初始化相关 *************************************************************************************************** -@ls_api.route('/api/ls/realtimemsg', methods=["GET", "POST"]) +@ls_api.api_route('/realtimemsg', methods=["GET", "POST"]) @error9999 def get_real_time_msg(): """ 获取实时消息 使用 merge_real_time_db()函数 :return: """ - my_wxid = get_conf(g.caf, g.at, "last") + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - merge_path = get_conf(g.caf, my_wxid, "merge_path") - key = get_conf(g.caf, my_wxid, "key") - wx_path = get_conf(g.caf, my_wxid, "wx_path") + merge_path = gc.get_conf(my_wxid, "merge_path") + key = gc.get_conf(my_wxid, "key") + wx_path = gc.get_conf(my_wxid, "wx_path") if not merge_path or not key or not wx_path or not wx_path: return ReJson(1002, body="msg_path or media_path or wx_path or key is required") - real_time_exe_path = get_conf(g.caf, g.at, "real_time_exe_path") + real_time_exe_path = gc.get_conf(gc.at, "real_time_exe_path") code, ret = all_merge_real_time_db(key=key, wx_path=wx_path, merge_path=merge_path, real_time_exe_path=real_time_exe_path) @@ -216,7 +229,7 @@ def get_real_time_msg(): # start 这部分为专业工具的api ********************************************************************************************* -@ls_api.route('/api/ls/wxinfo', methods=["GET", 'POST']) +@ls_api.api_route('/wxinfo', methods=["GET", 'POST']) @error9999 def get_wxinfo(): """ @@ -224,24 +237,33 @@ def get_wxinfo(): :return: """ import pythoncom - pythoncom.CoInitialize() + from pywxdump import WX_OFFS + pythoncom.CoInitialize() # 初始化COM库 wxinfos = get_wx_info(WX_OFFS) - pythoncom.CoUninitialize() + pythoncom.CoUninitialize() # 释放COM库 return ReJson(0, wxinfos) -@ls_api.route('/api/ls/biasaddr', methods=["GET", 'POST']) +class BiasAddrRequest(BaseModel): + mobile: str + name: str + account: str + key: str = "" + wxdbPath: str = "" + + +@ls_api.post('/biasaddr') @error9999 -def biasaddr(): +def biasaddr(request: BiasAddrRequest): """ BiasAddr :return: """ - mobile = request.json.get("mobile") - name = request.json.get("name") - account = request.json.get("account") - key = request.json.get("key", "") - wxdbPath = request.json.get("wxdbPath", "") + mobile = request.mobile + name = request.name + account = request.account + key = request.json.key + wxdbPath = request.wxdbPath if not mobile or not name or not account: return ReJson(1002) pythoncom.CoInitialize() @@ -249,39 +271,33 @@ def biasaddr(): return ReJson(0, str(rdata)) -@ls_api.route('/api/ls/decrypt', methods=["GET", 'POST']) +@ls_api.api_route('/decrypt', methods=["GET", 'POST']) @error9999 -def decrypt(): +def decrypt(key: str, wxdbPath: str, outPath: str = ""): """ 解密 :return: """ - key = request.json.get("key") - if not key: - return ReJson(1002) - wxdb_path = request.json.get("wxdbPath") - if not wxdb_path: - return ReJson(1002) - out_path = request.json.get("outPath") - if not out_path: - out_path = g.tmp_path - wxinfos = batch_decrypt(key, wxdb_path, out_path=out_path) + if not outPath: + outPath = gc.work_path + wxinfos = batch_decrypt(key, wxdbPath, out_path=outPath) return ReJson(0, str(wxinfos)) -@ls_api.route('/api/ls/merge', methods=["GET", 'POST']) +class MergeRequest(BaseModel): + dbPath: str + outPath: str + + +@ls_api.post('/merge') @error9999 -def merge(): +def merge(request: MergeRequest): """ 合并 :return: """ - wxdb_path = request.json.get("dbPath") - if not wxdb_path: - return ReJson(1002) - out_path = request.json.get("outPath") - if not out_path: - return ReJson(1002) + wxdb_path = request.dbPath + out_path = request.outPath db_path = get_wx_db(wxdb_path) # for i in db_path:print(i) rdata = merge_db(db_path, out_path) diff --git a/pywxdump/api/remote_server.py b/pywxdump/api/remote_server.py index 91baf21..35fcff0 100644 --- a/pywxdump/api/remote_server.py +++ b/pywxdump/api/remote_server.py @@ -5,43 +5,38 @@ # Author: xaoyaoo # Date: 2024/01/02 # ------------------------------------------------------------------------------- -import base64 -import json -import logging import os -import re import time import shutil from collections import Counter +from urllib.parse import quote +from typing import List, Optional + +from pydantic import BaseModel +from fastapi import APIRouter,Response +from starlette.responses import StreamingResponse, FileResponse -import pythoncom import pywxdump - -from flask import Flask, request, render_template, g, Blueprint, send_file, make_response, session -from pywxdump import get_core_db, all_merge_real_time_db -from pywxdump.api.rjson import ReJson, RqJson -from pywxdump.api.utils import get_conf, get_conf_wxids, set_conf, error9999, gen_base64, validate_title, \ - get_conf_local_wxid -from pywxdump import get_wx_info, WX_OFFS, batch_decrypt, BiasAddr, merge_db, decrypt_merge, merge_real_time_db - +from pywxdump import decrypt_merge,get_core_db from pywxdump.db import DBHandler, download_file, dat2img from pywxdump.db.export import export_csv, export_json, export_html -# app = Flask(__name__, static_folder='../ui/web/dist', static_url_path='/') +from .rjson import ReJson, RqJson +from .utils import error9999, gc, asyncError9999 -rs_api = Blueprint('rs_api', __name__, template_folder='../ui/web', static_folder='../ui/web/assets/', ) -rs_api.debug = False +rs_api = APIRouter() # 是否初始化 -@rs_api.route('/api/rs/is_init', methods=["GET", 'POST']) +@rs_api.api_route('/is_init', methods=["GET", 'POST']) @error9999 def is_init(): """ 是否初始化 :return: """ - local_wxids = get_conf_local_wxid(g.caf) + + local_wxids = gc.get_local_wxids() if len(local_wxids) > 1: return ReJson(0, True) return ReJson(0, False) @@ -49,72 +44,62 @@ def is_init(): # start 以下为聊天联系人相关api ******************************************************************************************* -@rs_api.route('/api/rs/mywxid', methods=["GET", 'POST']) +@rs_api.api_route('/mywxid', methods=["GET", 'POST']) @error9999 def mywxid(): """ 获取我的微信id :return: """ - my_wxid = get_conf(g.caf, g.at, "last") + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") return ReJson(0, {"my_wxid": my_wxid}) -@rs_api.route('/api/rs/user_session_list', methods=["GET", 'POST']) +@rs_api.api_route('/user_session_list', methods=["GET", 'POST']) @error9999 def user_session_list(): """ 获取联系人列表 :return: """ - my_wxid = get_conf(g.caf, g.at, "last") + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") + db_config = gc.get_conf(my_wxid, "db_config") db = DBHandler(db_config, my_wxid=my_wxid) ret = db.get_session_list() return ReJson(0, list(ret.values())) -@rs_api.route('/api/rs/user_labels_dict', methods=["GET", 'POST']) +@rs_api.api_route('/user_labels_dict', methods=["GET", 'POST']) @error9999 def user_labels_dict(): """ 获取标签字典 :return: """ - my_wxid = get_conf(g.caf, g.at, "last") + + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") + db_config = gc.get_conf(my_wxid, "db_config") db = DBHandler(db_config, my_wxid=my_wxid) user_labels_dict = db.get_labels() return ReJson(0, user_labels_dict) -@rs_api.route('/api/rs/user_list', methods=["GET", 'POST']) +@rs_api.post('/user_list') @error9999 -def user_list(): +def user_list(word: str = "", wxids: List[str] = None, labels: List[str] = None): """ 获取联系人列表,可用于搜索 :return: """ - if request.method == "GET": - word = request.args.get("word", "") - wxids = request.args.get("wxids", []) - labels = request.args.get("labels", []) - elif request.method == "POST": - word = request.json.get("word", "") - wxids = request.json.get("wxids", []) - labels = request.json.get("labels", []) - else: - return ReJson(1003, msg="Unsupported method") - if isinstance(wxids, str) and wxids == '' or wxids is None: wxids = [] if isinstance(labels, str) and labels == '' or labels is None: labels = [] - my_wxid = get_conf(g.caf, g.at, "last") + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") + db_config = gc.get_conf(my_wxid, "db_config") db = DBHandler(db_config, my_wxid=my_wxid) users = db.get_user(word, wxids, labels) return ReJson(0, users) @@ -124,26 +109,72 @@ def user_list(): # start 以下为聊天记录相关api ********************************************************************************************* +class MsgCountRequest(BaseModel): + wxids: Optional[List[str]] -@rs_api.route('/api/rs/imgsrc/', methods=["GET", 'POST']) + +@rs_api.post('/msg_count') @error9999 -def get_imgsrc(imgsrc): +def msg_count(request: MsgCountRequest): + """ + 获取联系人的聊天记录数量 + :return: + """ + wxids = request.wxids + my_wxid = gc.get_conf(gc.at, "last") + if not my_wxid: return ReJson(1001, body="my_wxid is required") + db_config = gc.get_db_config() + db = DBHandler(db_config, my_wxid=my_wxid) + count = db.get_msgs_count(wxids) + return ReJson(0, count) + + +class MsgListRequest(BaseModel): + wxid: str + start: int + limit: int + + +@rs_api.api_route('/msg_list', methods=["GET", 'POST']) +@error9999 +def get_msgs(request: MsgListRequest): + """ + 获取联系人的聊天记录 + :return: + """ + wxid = request.wxid + start = request.start + limit = request.limit + + my_wxid = gc.get_conf(gc.at, "last") + if not my_wxid: return ReJson(1001, body="my_wxid is required") + db_config = gc.get_conf(my_wxid, "db_config") + + db = DBHandler(db_config, my_wxid=my_wxid) + msgs, users = db.get_msgs(wxid=wxid, start_index=start, page_size=limit) + return ReJson(0, {"msg_list": msgs, "user_list": users}) + + +@rs_api.get('/imgsrc') +@asyncError9999 +async def get_imgsrc(src: str): """ 获取图片, 1. 从网络获取图片,主要功能只是下载图片,缓存到本地 2. 读取本地图片 :return: """ + imgsrc = src if not imgsrc: return ReJson(1002) if imgsrc.startswith("FileStorage"): # 如果是本地图片文件则调用get_img - my_wxid = get_conf(g.caf, g.at, "last") + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - wx_path = get_conf(g.caf, my_wxid, "wx_path") + wx_path = gc.get_conf(my_wxid, "wx_path") img_path = imgsrc.replace("\\\\", "\\") - img_tmp_path = os.path.join(g.work_path, my_wxid, "img") + img_tmp_path = os.path.join(gc.work_path, my_wxid, "img") original_img_path = os.path.join(wx_path, img_path) if os.path.exists(original_img_path): rc, fomt, md5, out_bytes = dat2img(original_img_path) @@ -151,21 +182,21 @@ def get_imgsrc(imgsrc): return ReJson(1001, body=original_img_path) imgsavepath = os.path.join(str(img_tmp_path), img_path + "_" + "".join([md5, fomt])) if os.path.exists(imgsavepath): - return send_file(imgsavepath) + return FileResponse(imgsavepath) if not os.path.exists(os.path.dirname(imgsavepath)): os.makedirs(os.path.dirname(imgsavepath)) with open(imgsavepath, "wb") as f: f.write(out_bytes) - return send_file(imgsavepath) + return Response(content=out_bytes, media_type="image/jpeg") else: return ReJson(1001, body=f"{original_img_path} not exists") elif imgsrc.startswith("http://") or imgsrc.startswith("https://"): # 将?后面的参数连接到imgsrc - imgsrc = imgsrc + "?" + request.query_string.decode("utf-8") if request.query_string else imgsrc - my_wxid = get_conf(g.caf, g.at, "last") + + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - img_tmp_path = os.path.join(g.work_path, my_wxid, "imgsrc") + img_tmp_path = os.path.join(gc.work_path, my_wxid, "imgsrc") if not os.path.exists(img_tmp_path): os.makedirs(img_tmp_path) file_name = imgsrc.replace("http://", "").replace("https://", "").replace("/", "_").replace("?", "_") @@ -176,97 +207,69 @@ def get_imgsrc(imgsrc): img_path_all = os.path.join(str(img_tmp_path), file_name) if os.path.exists(img_path_all): - return send_file(img_path_all) + return FileResponse(img_path_all) else: - download_file(imgsrc, img_path_all) + # proxies = { + # "http": "http://127.0.0.1:10809", + # "https": "http://127.0.0.1:10809", + # } + proxies = None + download_file(imgsrc, img_path_all, proxies=proxies) if os.path.exists(img_path_all): - return send_file(img_path_all) + return FileResponse(img_path_all) else: return ReJson(4004, body=imgsrc) else: return ReJson(1002, body=imgsrc) -@rs_api.route('/api/rs/msg_count', methods=["GET", 'POST']) -@error9999 -def msg_count(): +@rs_api.api_route('/video', methods=["GET", 'POST']) +def get_video(src: str): """ - 获取联系人的聊天记录数量 + 获取视频 :return: """ - if request.method == "GET": - wxid = request.args.get("wxids", []) - elif request.method == "POST": - wxid = request.json.get("wxids", []) - else: - return ReJson(1003, msg="Unsupported method") - - my_wxid = get_conf(g.caf, g.at, "last") + videoPath = src + if not videoPath: + return ReJson(1002) + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") - db = DBHandler(db_config, my_wxid=my_wxid) - count = db.get_msgs_count(wxid) - return ReJson(0, count) - - -@rs_api.route('/api/rs/msg_list', methods=["GET", 'POST']) -@error9999 -def get_msgs(): - my_wxid = get_conf(g.caf, g.at, "last") - if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") - - start = request.json.get("start") - limit = request.json.get("limit") - wxid = request.json.get("wxid") - - if not wxid: - return ReJson(1002, body=f"wxid is required: {wxid}") - if start and isinstance(start, str) and start.isdigit(): - start = int(start) - if limit and isinstance(limit, str) and limit.isdigit(): - limit = int(limit) - if start is None or limit is None: - return ReJson(1002, body=f"start or limit is required {start} {limit}") - if not isinstance(start, int) and not isinstance(limit, int): - return ReJson(1002, body=f"start or limit is not int {start} {limit}") - - db = DBHandler(db_config, my_wxid=my_wxid) - msgs, users = db.get_msgs(wxid=wxid, start_index=start, page_size=limit) - return ReJson(0, {"msg_list": msgs, "user_list": users}) - - -@rs_api.route('/api/rs/video/', methods=["GET", 'POST']) -def get_video(videoPath): - my_wxid = get_conf(g.caf, g.at, "last") - if not my_wxid: return ReJson(1001, body="my_wxid is required") - wx_path = get_conf(g.caf, my_wxid, "wx_path") + wx_path = gc.get_conf(my_wxid, "wx_path") videoPath = videoPath.replace("\\\\", "\\") - video_tmp_path = os.path.join(g.work_path, my_wxid, "video") + video_tmp_path = os.path.join(gc.work_path, my_wxid, "video") original_img_path = os.path.join(wx_path, videoPath) if not os.path.exists(original_img_path): return ReJson(5002) # 复制文件到临时文件夹 + assert isinstance(video_tmp_path, str) video_save_path = os.path.join(video_tmp_path, videoPath) if not os.path.exists(os.path.dirname(video_save_path)): os.makedirs(os.path.dirname(video_save_path)) if os.path.exists(video_save_path): - return send_file(video_save_path) + return FileResponse(path=video_save_path) shutil.copy(original_img_path, video_save_path) - return send_file(original_img_path) + return FileResponse(path=video_save_path) -@rs_api.route('/api/rs/audio/', methods=["GET", 'POST']) -def get_audio(savePath): - my_wxid = get_conf(g.caf, g.at, "last") +@rs_api.api_route('/audio', methods=["GET", 'POST']) +def get_audio(src: str): + """ + 获取语音 + :return: + """ + savePath = src.replace("audio\\", "") + if not savePath: + return ReJson(1002) + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") + db_config = gc.get_conf(my_wxid, "db_config") - savePath = os.path.join(g.work_path, my_wxid, "audio", savePath) # 这个是从url中获取的 + savePath = os.path.join(gc.work_path, my_wxid, "audio", savePath) # 这个是从url中获取的 if os.path.exists(savePath): - return send_file(savePath) + assert isinstance(savePath, str) + return FileResponse(path=savePath, media_type='audio/mpeg') MsgSvrID = savePath.split("_")[-1].replace(".wav", "") if not savePath: @@ -282,21 +285,25 @@ def get_audio(savePath): return ReJson(1001, body="wave_data is required") if os.path.exists(savePath): - return send_file(savePath) + assert isinstance(savePath, str) + return FileResponse(path=savePath, media_type='audio/mpeg') else: return ReJson(4004, body=savePath) -@rs_api.route('/api/rs/file_info', methods=["GET", 'POST']) -def get_file_info(): - file_path = request.args.get("file_path") - file_path = request.json.get("file_path", file_path) +class FileInfoRequest(BaseModel): + file_path: str + + +@rs_api.api_route('/file_info', methods=["GET", 'POST']) +def get_file_info(request: FileInfoRequest): + file_path = request.file_path if not file_path: return ReJson(1002) - my_wxid = get_conf(g.caf, g.at, "last") + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - wx_path = get_conf(g.caf, my_wxid, "wx_path") + wx_path = gc.get_conf(my_wxid, "wx_path") all_file_path = os.path.join(wx_path, file_path) if not os.path.exists(all_file_path): @@ -306,34 +313,59 @@ def get_file_info(): return ReJson(0, {"file_name": file_name, "file_size": str(file_size)}) -@rs_api.route('/api/rs/file/', methods=["GET", 'POST']) -def get_file(filePath): - my_wxid = get_conf(g.caf, g.at, "last") +@rs_api.get('/file') +def get_file(src: str): + """ + 获取文件 + :return: + """ + file_path = src + if not file_path: + return ReJson(1002) + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - wx_path = get_conf(g.caf, my_wxid, "wx_path") + wx_path = gc.get_conf(my_wxid, "wx_path") - all_file_path = os.path.join(wx_path, filePath) + all_file_path = os.path.join(wx_path, file_path) if not os.path.exists(all_file_path): return ReJson(5002) - return send_file(all_file_path) + + def file_iterator(file_path, chunk_size=8192): + with open(file_path, "rb") as f: + while True: + chunk = f.read(chunk_size) + if not chunk: + break + yield chunk + + headers = { + "Content-Disposition": f'attachment; filename*=UTF-8\'\'{quote(os.path.basename(all_file_path))}', + } + return StreamingResponse(file_iterator(all_file_path), media_type="application/octet-stream", headers=headers) # end 以上为聊天记录相关api ********************************************************************************************* # start 导出聊天记录 ***************************************************************************************************** +class ExportEndbRequest(BaseModel): + wx_path: str = "" + outpath: str = "" + key: str = "" -@rs_api.route('/api/rs/export_endb', methods=["GET", 'POST']) -def get_export_endb(): + +@rs_api.api_route('/export_endb', methods=["GET", 'POST']) +def get_export_endb(request: ExportEndbRequest): """ 导出加密数据库 :return: """ - my_wxid = get_conf(g.caf, g.at, "last") + + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - wx_path = request.json.get("wx_path", "") + wx_path = request.wx_path if not wx_path: - wx_path = get_conf(g.caf, my_wxid, "wx_path") + wx_path = gc.get_conf(my_wxid, "wx_path") if not os.path.exists(wx_path if wx_path else ""): return ReJson(1002, body=f"wx_path is required: {wx_path}") @@ -342,7 +374,7 @@ def get_export_endb(): if not code: return ReJson(2001, body=wxdbpaths) - outpath = os.path.join(g.work_path, "export", my_wxid, "endb") + outpath = os.path.join(gc.work_path, "export", my_wxid, "endb") if not os.path.exists(outpath): os.makedirs(outpath) @@ -354,25 +386,28 @@ def get_export_endb(): return ReJson(0, body=outpath) -@rs_api.route('/api/rs/export_dedb', methods=["GET", "POST"]) -def get_export_dedb(): +class ExportDedbRequest(BaseModel): + wx_path: str = "" + outpath: str = "" + key: str = "" + + +@rs_api.api_route('/export_dedb', methods=["GET", "POST"]) +def get_export_dedb(request: ExportDedbRequest): """ 导出解密数据库 :return: """ - if request.method not in ["GET", "POST"]: - return ReJson(1003, msg="Unsupported method") - rq_data = request.json if request.method == "POST" else request.args - key = rq_data.get("key", "") - wx_path = rq_data.get("wx_path", "") + key = request.key + wx_path = request.wx_path - my_wxid = get_conf(g.caf, g.at, "last") + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") if not key: - key = get_conf(g.caf, my_wxid, "key") + key = gc.get_conf(my_wxid, "key") if not wx_path: - wx_path = get_conf(g.caf, my_wxid, "wx_path") + wx_path = gc.get_conf(my_wxid, "wx_path") if not key: return ReJson(1002, body=f"key is required: {key}") @@ -381,7 +416,7 @@ def get_export_dedb(): if not os.path.exists(wx_path): return ReJson(1001, body=f"wx_path not exists: {wx_path}") - outpath = os.path.join(g.work_path, "export", my_wxid, "dedb") + outpath = os.path.join(gc.work_path, "export", my_wxid, "dedb") if not os.path.exists(outpath): os.makedirs(outpath) assert isinstance(outpath, str) @@ -393,17 +428,22 @@ def get_export_dedb(): return ReJson(2001, body=merge_save_path) -@rs_api.route('/api/rs/export_csv', methods=["GET", 'POST']) -def get_export_csv(): +class ExportCsvRequest(BaseModel): + wxid: str + + +@rs_api.api_route('/export_csv', methods=["GET", 'POST']) +def get_export_csv(request: ExportCsvRequest): """ 导出csv :return: """ - my_wxid = get_conf(g.caf, g.at, "last") - if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") - wxid = request.json.get("wxid") + my_wxid = gc.get_conf(gc.at, "last") + if not my_wxid: return ReJson(1001, body="my_wxid is required") + db_config = gc.get_conf(my_wxid, "db_config") + + wxid = request.wxid # st_ed_time = request.json.get("datetime", [0, 0]) if not wxid: return ReJson(1002, body=f"username is required: {wxid}") @@ -413,7 +453,7 @@ def get_export_csv(): # if not isinstance(start, int) or not isinstance(end, int) or start >= end: # return ReJson(1002, body=f"datetime is required: {st_ed_time}") - outpath = os.path.join(g.work_path, "export", my_wxid, "csv", wxid) + outpath = os.path.join(gc.work_path, "export", my_wxid, "csv", wxid) if not os.path.exists(outpath): os.makedirs(outpath) @@ -424,21 +464,26 @@ def get_export_csv(): return ReJson(2001, body=ret) -@rs_api.route('/api/rs/export_json', methods=["GET", 'POST']) -def get_export_json(): +class ExportJsonRequest(BaseModel): + wxid: str + + +@rs_api.api_route('/export_json', methods=["GET", 'POST']) +def get_export_json(request: ExportJsonRequest): """ 导出json :return: """ - my_wxid = get_conf(g.caf, g.at, "last") - if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") - wxid = request.json.get("wxid") + my_wxid = gc.get_conf(gc.at, "last") + if not my_wxid: return ReJson(1001, body="my_wxid is required") + db_config = gc.get_conf(my_wxid, "db_config") + + wxid = request.wxid if not wxid: return ReJson(1002, body=f"username is required: {wxid}") - outpath = os.path.join(g.work_path, "export", my_wxid, "json", wxid) + outpath = os.path.join(gc.work_path, "export", my_wxid, "json", wxid) if not os.path.exists(outpath): os.makedirs(outpath) @@ -449,21 +494,26 @@ def get_export_json(): return ReJson(2001, body=ret) -@rs_api.route('/api/rs/export_html', methods=["GET", 'POST']) -def get_export_html(): +class ExportHtmlRequest(BaseModel): + wxid: str + + +@rs_api.api_route('/export_html', methods=["GET", 'POST']) +def get_export_html(request: ExportHtmlRequest): """ 导出json :return: """ - my_wxid = get_conf(g.caf, g.at, "last") - if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") - wxid = request.json.get("wxid") + my_wxid = gc.get_conf(gc.at, "last") + if not my_wxid: return ReJson(1001, body="my_wxid is required") + db_config = gc.get_conf(my_wxid, "db_config") + + wxid = request.wxid if not wxid: return ReJson(1002, body=f"username is required: {wxid}") - html_outpath = os.path.join(g.work_path, "export", my_wxid, "html") + html_outpath = os.path.join(gc.work_path, "export", my_wxid, "html") if not os.path.exists(html_outpath): os.makedirs(html_outpath) assert isinstance(html_outpath, str) @@ -474,7 +524,6 @@ def get_export_html(): web_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "ui", "web") shutil.copytree(web_path, outpath) - code, ret = export_html(wxid, outpath, db_config, my_wxid=my_wxid) if code: @@ -486,69 +535,75 @@ def get_export_html(): # end 导出聊天记录 ******************************************************************************************************* # start 聊天记录分析api ************************************************************************************************** +class DateCountRequest(BaseModel): + wxid: str = "" + start_time: int = 0 + end_time: int = 0 + time_format: str = "%Y-%m-%d" -@rs_api.route('/api/rs/date_count', methods=["GET", 'POST']) -def get_date_count(): + +@rs_api.api_route('/date_count', methods=["GET", 'POST']) +def get_date_count(request: DateCountRequest): """ 获取日期统计 :return: """ - if request.method not in ["GET", "POST"]: - return ReJson(1003, msg="Unsupported method") - rq_data = request.json if request.method == "POST" else request.args - word = rq_data.get("wxid", "") - start_time = rq_data.get("start_time", 0) - end_time = rq_data.get("end_time", 0) - time_format = rq_data.get("time_format", "%Y-%m-%d") + wxid = request.wxid + start_time = request.start_time + end_time = request.end_time + time_format = request.time_format - my_wxid = get_conf(g.caf, g.at, "last") + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") + db_config = gc.get_conf(my_wxid, "db_config") db = DBHandler(db_config, my_wxid=my_wxid) - date_count = db.get_date_count(wxid=word, start_time=start_time, end_time=end_time, time_format=time_format) + date_count = db.get_date_count(wxid=wxid, start_time=start_time, end_time=end_time, time_format=time_format) return ReJson(0, date_count) -@rs_api.route('/api/rs/top_talker_count', methods=["GET", 'POST']) -def get_top_talker_count(): +class TopTalkerCountRequest(BaseModel): + top: int = 10 + start_time: int = 0 + end_time: int = 0 + + +@rs_api.api_route('/top_talker_count', methods=["GET", 'POST']) +def get_top_talker_count(request: TopTalkerCountRequest): """ 获取最多聊天的人 :return: """ - if request.method not in ["GET", "POST"]: - return ReJson(1003, msg="Unsupported method") - rq_data = request.json if request.method == "POST" else request.args - top = rq_data.get("top", 10) - start_time = rq_data.get("start_time", 0) - end_time = rq_data.get("end_time", 0) + top = request.top + start_time = request.start_time + end_time = request.end_time - my_wxid = get_conf(g.caf, g.at, "last") + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") + db_config = gc.get_conf(my_wxid, "db_config") date_count = DBHandler(db_config, my_wxid=my_wxid).get_top_talker_count(top=top, start_time=start_time, end_time=end_time) return ReJson(0, date_count) -@rs_api.route('/api/rs/wordcloud', methods=["GET", 'POST']) -@error9999 -def wordcloud(): - if request.method not in ["GET", "POST"]: - return ReJson(1003, msg="Unsupported method") - rq_data = request.json if request.method == "POST" else request.args +class WordCloudRequest(BaseModel): + target: str = "signature" + +@rs_api.api_route('/wordcloud', methods=["GET", 'POST']) +@error9999 +def get_wordcloud(request: WordCloudRequest): try: import jieba except ImportError: return ReJson(9999, body="jieba is required") - target = rq_data.get("target", "") + target = request.target if not target: return ReJson(1002, body="target is required") - my_wxid = get_conf(g.caf, g.at, "last") + my_wxid = gc.get_conf(gc.at, "last") if not my_wxid: return ReJson(1001, body="my_wxid is required") - db_config = get_conf(g.caf, my_wxid, "db_config") + db_config = gc.get_conf(my_wxid, "db_config") db = DBHandler(db_config, my_wxid=my_wxid) if target == "signature": @@ -583,7 +638,7 @@ def wordcloud(): # end 聊天记录分析api **************************************************************************************************** # 关于、帮助、设置 ******************************************************************************************************* -@rs_api.route('/api/rs/check_update', methods=["GET", 'POST']) +@rs_api.api_route('/check_update', methods=["GET", 'POST']) @error9999 def check_update(): """ @@ -609,7 +664,7 @@ def check_update(): return ReJson(9999, msg=str(e)) -@rs_api.route('/api/rs/version', methods=["GET", 'POST']) +@rs_api.api_route('/version', methods=["GET", "POST"]) @error9999 def version(): """ @@ -619,7 +674,7 @@ def version(): return ReJson(0, pywxdump.__version__) -@rs_api.route('/api/rs/get_readme', methods=["GET", 'POST']) +@rs_api.api_route('/get_readme', methods=["GET", 'POST']) @error9999 def get_readme(): """ @@ -638,9 +693,3 @@ def get_readme(): # END 关于、帮助、设置 *************************************************************************************************** - - -@rs_api.route('/') -@error9999 -def index(): - return render_template('index.html') diff --git a/pywxdump/api/utils.py b/pywxdump/api/utils.py index 852aa04..0ea8a5c 100644 --- a/pywxdump/api/utils.py +++ b/pywxdump/api/utils.py @@ -16,8 +16,122 @@ from .rjson import ReJson from functools import wraps import logging -rs_loger = logging.getLogger("rs_api") -ls_loger = logging.getLogger("ls_api") +server_loger = logging.getLogger("server") +rs_loger = server_loger +ls_loger = server_loger + + +class ConfData(object): + _instances = None + + def __new__(cls, *args, **kwargs): + if cls._instances: + return cls._instances + cls._instances = object.__new__(cls) + return cls._instances + + def __init__(self): + self._work_path = None + self.conf_file = None + self.auto_setting = None + + self.is_init = False + + self.conf = {} + + self.init() + + @property + def cf(self): + if not self.is_init: + self.init() + return self.conf_file + + @property + def work_path(self): + if not self.is_init: + self.init() + return self._work_path + + @property + def at(self): + if not self.is_init: + self.init() + return self.auto_setting + + def init(self): + self.is_init = False + + work_path = os.getenv("PYWXDUMP_WORK_PATH") + conf_file = os.getenv("PYWXDUMP_CONF_FILE") + auto_setting = os.getenv("PYWXDUMP_AUTO_SETTING") + + if work_path is None or conf_file is None or auto_setting is None: + return False + + self._work_path = work_path + self.conf_file = conf_file + self.auto_setting = auto_setting + if not os.path.exists(self.conf_file): + self.set_conf(self.auto_setting, "last", "") + self.is_init = True + self.read_conf() + return True + + def read_conf(self): + if not self.is_init: + self.init() + try: + with open(self.conf_file, 'r') as f: + conf = json.load(f) + self.conf = conf + return True + except FileNotFoundError: + logging.error(f"Session file not found: {self.conf_file}") + return False + except json.JSONDecodeError as e: + logging.error(f"Error decoding JSON file: {e}") + return False + + def write_conf(self): + if not self.is_init: + self.init() + try: + with open(self.conf_file, 'w') as f: + json.dump(self.conf, f, indent=4, ensure_ascii=False) + return True + except Exception as e: + logging.error(f"Error writing to file: {e}") + return False + + def set_conf(self, wxid, arg, value): + if not self.is_init: + self.init() + if wxid not in self.conf: + self.conf[wxid] = {} + if not isinstance(self.conf[wxid], dict): + self.conf[wxid] = {} + self.conf[wxid][arg] = value + self.write_conf() + + def get_conf(self, wxid, arg): + if not self.is_init: + self.init() + return self.conf.get(wxid, {}).get(arg, None) + + def get_local_wxids(self): + if not self.is_init: + self.init() + return list(self.conf.keys()) + + def get_db_config(self): + if not self.is_init: + self.init() + my_wxid = self.get_conf(self.at, "last") + return self.get_conf(my_wxid, "db_config") + + +gc: ConfData = ConfData() def get_conf_local_wxid(conf_file): @@ -83,6 +197,16 @@ def set_conf(conf_file, wxid, arg, value): return True +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 + + def validate_title(title): """ 校验文件名是否合法 @@ -106,6 +230,20 @@ def error9999(func): return wrapper +def asyncError9999(func): + @wraps(func) + async def wrapper(*args, **kwargs): + try: + return await func(*args, **kwargs) + except Exception as e: + traceback_data = traceback.format_exc() + rdata = f"{traceback_data}" + # logging.error(rdata) + return ReJson(9999, body=f"{str(e)}\n{rdata}", error=str(e)) + + return wrapper + + def gen_base64(path): # 获取文件名后缀 extension = os.path.splitext(path)[1] diff --git a/pywxdump/cli.py b/pywxdump/cli.py index a9fa61c..0b4af51 100644 --- a/pywxdump/cli.py +++ b/pywxdump/cli.py @@ -6,8 +6,8 @@ # Date: 2023/10/14 # ------------------------------------------------------------------------------- import argparse +import os import sys -import time from pywxdump import * import pywxdump @@ -289,7 +289,7 @@ class MainShowChatRecords(BaseSubMainClass): print("[-] 输入数据库路径不存在") return - start_falsk(merge_path=merge_path, wx_path=args.wx_path, key="", my_wxid=args.my_wxid, online=online) + start_server(merge_path=merge_path, wx_path=args.wx_path, key="", my_wxid=args.my_wxid, online=online) class MainExportChatRecords(BaseSubMainClass): @@ -327,7 +327,7 @@ class MainUi(BaseSubMainClass): parser.add_argument("-p", '--port', metavar="", type=int, help="(可选)端口号", default=5000) parser.add_argument("--online", help="(可选)是否在线查看(局域网查看)", default=False, action='store_true') parser.add_argument("--debug", help="(可选)是否开启debug模式", default=False, action='store_true') - parser.add_argument("--noOpenBrowser", dest='isOpenBrowser', action='store_false', default=True, + parser.add_argument("--noOpenBrowser", dest='isOpenBrowser', default=True, action='store_false', help="(可选)用于禁用自动打开浏览器") return parser @@ -339,7 +339,7 @@ class MainUi(BaseSubMainClass): debug = args.debug isopenBrowser = args.isOpenBrowser - start_falsk(port=port, online=online, debug=debug, isopenBrowser=isopenBrowser) + start_server(port=port, online=online, debug=debug, isopenBrowser=isopenBrowser) class MainApi(BaseSubMainClass): @@ -360,7 +360,7 @@ class MainApi(BaseSubMainClass): port = args.port debug = args.debug - start_falsk(port=port, online=online, debug=debug, isopenBrowser=False) + start_server(port=port, online=online, debug=debug, isopenBrowser=False) def console_run(): diff --git a/pywxdump/db/dbMSG.py b/pywxdump/db/dbMSG.py index c3e9e22..d3d6623 100644 --- a/pywxdump/db/dbMSG.py +++ b/pywxdump/db/dbMSG.py @@ -242,7 +242,7 @@ class MsgHandler(DatabaseBase): voicelength = int(voicelength) / 1000 voicelength = f"{voicelength:.2f}" msg = f"语音时长:{voicelength}秒\n翻译结果:{transtext}" if transtext else f"语音时长:{voicelength}秒" - src = os.path.join("audio", f"{StrTalker}", + src = os.path.join(f"{StrTalker}", f"{CreateTime.replace(':', '-').replace(' ', '_')}_{IsSender}_{MsgSvrID}.wav") elif type_id == (43, 0): # 视频 DictExtra = get_BytesExtra(BytesExtra) diff --git a/pywxdump/db/dbbase.py b/pywxdump/db/dbbase.py index dfce4e6..bfedbc1 100644 --- a/pywxdump/db/dbbase.py +++ b/pywxdump/db/dbbase.py @@ -96,8 +96,11 @@ class DatabaseBase(DatabaseSingletonBase): def __get_existed_tables(self): sql = "SELECT tbl_name FROM sqlite_master WHERE type = 'table' and tbl_name!='sqlite_sequence';" existing_tables = self.execute(sql) - self.existed_tables = [row[0].lower() for row in existing_tables] - return self.existed_tables + if existing_tables: + self.existed_tables = [row[0].lower() for row in existing_tables] + return self.existed_tables + else: + return None def tables_exist(self, required_tables: str or list): """ diff --git a/pywxdump/db/utils/common_utils.py b/pywxdump/db/utils/common_utils.py index cf64671..f98b649 100644 --- a/pywxdump/db/utils/common_utils.py +++ b/pywxdump/db/utils/common_utils.py @@ -252,18 +252,19 @@ def xml2dict(xml_string): return parse_xml(root) -def download_file(url, save_path=None): +def download_file(url, save_path=None, proxies=None): """ 下载文件 :param url: 文件下载地址 :param save_path: 保存路径 + :param proxies: requests 代理 :return: 保存路径 """ headers = { "User-Agent": "Mozilla/5.0 (Linux; Android 10; Redmi K40 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Mobile Safari/537.36" } - r = requests.get(url, headers=headers) + r = requests.get(url, headers=headers, proxies=proxies) if r.status_code != 200: return None data = r.content diff --git a/pywxdump/server.py b/pywxdump/server.py deleted file mode 100644 index 1e5f405..0000000 --- a/pywxdump/server.py +++ /dev/null @@ -1,143 +0,0 @@ -# -*- coding: utf-8 -*-# -# ------------------------------------------------------------------------------- -# Name: server.py -# Description: -# Author: xaoyaoo -# Date: 2024/01/04 -# ------------------------------------------------------------------------------- -import os -import subprocess -import sys -import time -import logging - -server_loger = logging.getLogger("server") - - -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 - - -def start_falsk(merge_path="", wx_path="", key="", my_wxid="", port=5000, online=False, debug=False, - isopenBrowser=True, loger_handler=None): - """ - 启动flask - :param merge_path: 合并后的数据库路径 - :param wx_path: 微信文件夹的路径(用于显示图片) - :param key: 密钥 - :param my_wxid: 微信账号(本人微信id) - :param port: 端口号 - :param online: 是否在线查看(局域网查看) - :param debug: 是否开启debug模式 - :param isopenBrowser: 是否自动打开浏览器 - :return: - """ - work_path = os.path.join(os.getcwd(), "wxdump_work") # 临时文件夹,用于存放图片等 - if not os.path.exists(work_path): - os.makedirs(work_path) - server_loger.info(f"[+] 创建临时文件夹:{work_path}") - print(f"[+] 创建临时文件夹:{work_path}") - - conf_auto_file = os.path.join(work_path, "conf_auto.json") # 用于存放各种基础信息 - at = "auto_setting" - - from flask import Flask, g - from flask_cors import CORS - from pywxdump.api import rs_api, ls_api, get_conf, set_conf - - # 检查端口是否被占用 - 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/') - with app.app_context(): - # 设置超时时间为 1000 秒 - app.config['TIMEOUT'] = 1000 - app.secret_key = 'secret_key' - - app.logger.setLevel(logging.WARNING) - if loger_handler: - app.logger.addHandler(loger_handler) - # 获取 Werkzeug 的日志记录器 - werkzeug_logger = logging.getLogger('werkzeug') - # 将自定义格式器应用到 Werkzeug 的日志记录器 - werkzeug_logger.addHandler(loger_handler) - werkzeug_logger.setLevel(logging.DEBUG) - - CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True) # 允许所有域名跨域 - - @app.after_request # 请求后的处理 用于解决部分用户浏览器不支持flask以及vue的js文件返回问题 - def changeHeader(response): - disposition = response.get_wsgi_headers('environ').get( - 'Content-Disposition') or '' # 获取返回头文件名描述,如'inline; filename=index.562b9b5a.js' - if disposition.rfind('.js') == len(disposition) - 3: - response.mimetype = 'application/javascript' - return response - - @app.before_request - def before_request(): - g.work_path = work_path # 临时文件夹,用于存放图片等-新版本 - g.caf = conf_auto_file # 用于存放各种基础信息-新版本 - g.at = at # 用于默认设置-新版本 - - if merge_path: - set_conf(conf_auto_file, at, "merge_path", merge_path) - db_config = { - "key": "merge_all", - "type": "sqlite", - "path": "D:\\_code\\py_code\\pywxdumpProject\\z_test\\wxdump_work\\wxid_zh12s67kxsqs22\\merge_all.db" - } - set_conf(conf_auto_file, at, "db_config", db_config) - if wx_path: set_conf(conf_auto_file, at, "wx_path", wx_path) - if key: set_conf(conf_auto_file, at, "key", key) - if my_wxid: set_conf(conf_auto_file, at, "my_wxid", my_wxid) - if not os.path.exists(conf_auto_file): - set_conf(conf_auto_file, at, "last", my_wxid) - - app.register_blueprint(rs_api) - app.register_blueprint(ls_api) - - if isopenBrowser: - 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: - server_loger.error(f"Unsupported platform, can't open browser automatically.", exc_info=True) - print("Unsupported platform, can't open browser automatically.") - except Exception as e: - server_loger.error(f"自动打开浏览器失败:{e}", exc_info=True) - - if is_port_in_use(host, port): - server_loger.error(f"Port {port} is already in use. Choose a different port.") - print(f"Port {port} is already in use. Choose a different port.") - input("Press Enter to exit...") - else: - time.sleep(1) - server_loger.info(f"启动flask服务,host:port:{host}:{port}") - print("[+] 请使用浏览器访问 http://127.0.0.1:5000/ 查看聊天记录") - app.run(host=host, port=port, debug=debug, threaded=False) - - -if __name__ == '__main__': - merge_path = r"****.db" - - wx_path = r"****" - my_wxid = "****" - - start_falsk(merge_path=merge_path, wx_path=wx_path, my_wxid=my_wxid, - port=5000, online=False, debug=False, isopenBrowser=False) diff --git a/requirements.txt b/requirements.txt index 325c432..5df9be5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,8 @@ blackboxprotobuf lz4 lxml flask_cors -pandas \ No newline at end of file +pandas + +fastapi +uvicorn +dotenv \ No newline at end of file