该flask为fastapi,速度更快

This commit is contained in:
xaoyaoo 2024-08-17 11:51:57 +08:00
parent 81832a2a99
commit 0b937a2f11
11 changed files with 657 additions and 462 deletions

View File

@ -5,15 +5,8 @@
# Author: xaoyaoo # Author: xaoyaoo
# Date: 2023/10/14 # Date: 2023/10/14
# ------------------------------------------------------------------------------- # -------------------------------------------------------------------------------
# from .analyzer.db_parsing import read_img_dat, read_emoji, decompress_CompressContent, read_audio_buf, read_audio, \ __version__ = "3.1.18"
# 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
import os, json import os, json
try: try:
@ -24,7 +17,18 @@ except:
WX_OFFS = {} WX_OFFS = {}
WX_OFFS_PATH = None 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__) # PYWXDUMP_ROOT_PATH = os.path.dirname(__file__)
# db_init = DBPool("DBPOOL_INIT") # 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"]

View File

@ -5,9 +5,132 @@
# Author: xaoyaoo # Author: xaoyaoo
# Date: 2023/12/14 # 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 .remote_server import rs_api
from .local_server import ls_api from .local_server import ls_api
from .utils import get_conf, set_conf
if __name__ == '__main__': from pywxdump import __version__
pass
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": "<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"]

View File

@ -5,58 +5,55 @@
# Author: xaoyaoo # Author: xaoyaoo
# Date: 2024/08/01 # Date: 2024/08/01
# ------------------------------------------------------------------------------- # -------------------------------------------------------------------------------
import base64
import json
import logging
import os import os
import re
import time import time
import shutil import shutil
import pythoncom import pythoncom
import pywxdump
from flask import Flask, request, render_template, g, Blueprint, send_file, make_response, session from pydantic import BaseModel
from pywxdump import get_core_db, all_merge_real_time_db, get_wx_db from fastapi import APIRouter
from pywxdump.api.rjson import ReJson, RqJson
from pywxdump.api.utils import get_conf, get_conf_wxids, set_conf, error9999, gen_base64, validate_title, \ from pywxdump import all_merge_real_time_db, get_wx_db
get_conf_local_wxid, ls_loger, random_str from pywxdump import get_wx_info, batch_decrypt, BiasAddr, merge_db, decrypt_merge
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/', ) from .rjson import ReJson, RqJson
ls_api.debug = False 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 @error9999
def init_last_local_wxid(): def init_last_local_wxid():
""" """
初始化包括key 初始化包括key
:return: :return:
""" """
local_wxid = get_conf_local_wxid(g.caf) local_wxid = gc.get_local_wxids()
local_wxid.remove(g.at) local_wxid.remove(gc.at)
if local_wxid: if local_wxid:
return ReJson(0, {"local_wxids": local_wxid}) return ReJson(0, {"local_wxids": local_wxid})
return ReJson(0, {"local_wxids": []}) 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 @error9999
def init_last(): def init_last(my_wxid: str):
""" """
是否初始化 是否初始化
:return: :return:
""" """
my_wxid = request.json.get("my_wxid", "")
my_wxid = my_wxid.strip().strip("'").strip('"') if isinstance(my_wxid, str) else "" my_wxid = my_wxid.strip().strip("'").strip('"') if isinstance(my_wxid, str) else ""
if not my_wxid: 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: if my_wxid:
set_conf(g.caf, "auto_setting", "last", my_wxid) gc.set_conf(gc.at, "last", my_wxid)
merge_path = get_conf(g.caf, my_wxid, "merge_path") merge_path = gc.get_conf(my_wxid, "merge_path")
wx_path = get_conf(g.caf, my_wxid, "wx_path") wx_path = gc.get_conf(my_wxid, "wx_path")
key = get_conf(g.caf, my_wxid, "key") key = gc.get_conf(my_wxid, "key")
rdata = { rdata = {
"merge_path": merge_path, "merge_path": merge_path,
"wx_path": wx_path, "wx_path": wx_path,
@ -69,16 +66,23 @@ def init_last():
return ReJson(0, {"is_init": False, "my_wxid": ""}) 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 @error9999
def init_key(): def init_key(request: InitKeyRequest):
""" """
初始化包括key 初始化key
:param request:
:return: :return:
""" """
wx_path = request.json.get("wx_path", "").strip().strip("'").strip('"') wx_path = request.wx_path.strip().strip("'").strip('"')
key = request.json.get("key", "").strip().strip("'").strip('"') key = request.key.strip().strip("'").strip('"')
my_wxid = request.json.get("my_wxid", "").strip().strip("'").strip('"') my_wxid = request.my_wxid.strip().strip("'").strip('"')
if not wx_path: if not wx_path:
return ReJson(1002, body=f"wx_path is required: {wx_path}") return ReJson(1002, body=f"wx_path is required: {wx_path}")
if not os.path.exists(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")): # if isinstance(db_config, dict) and db_config and os.path.exists(db_config.get("path")):
# pmsg = DBHandler(db_config) # pmsg = DBHandler(db_config)
# # pmsg.close_all_connection() # # 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): if os.path.exists(out_path):
try: try:
@ -107,9 +113,9 @@ def init_key():
time.sleep(1) time.sleep(1)
if code: if code:
# 移动merge_save_path到g.work_path/my_wxid # 移动merge_save_path到g.work_path/my_wxid
if not os.path.exists(os.path.join(g.work_path, my_wxid)): if not os.path.exists(os.path.join(gc.work_path, my_wxid)):
os.makedirs(os.path.join(g.work_path, my_wxid)) os.makedirs(os.path.join(gc.work_path, my_wxid))
merge_save_path_new = os.path.join(g.work_path, my_wxid, "merge_all.db") 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)) shutil.move(merge_save_path, str(merge_save_path_new))
# 删除out_path # 删除out_path
@ -124,12 +130,13 @@ def init_key():
"type": "sqlite", "type": "sqlite",
"path": merge_save_path_new "path": merge_save_path_new
} }
set_conf(g.caf, my_wxid, "db_config", db_config) gc.set_conf(my_wxid, "db_config", db_config)
set_conf(g.caf, my_wxid, "merge_path", merge_save_path_new) gc.set_conf(my_wxid, "db_config", db_config)
set_conf(g.caf, my_wxid, "wx_path", wx_path) gc.set_conf(my_wxid, "merge_path", merge_save_path_new)
set_conf(g.caf, my_wxid, "key", key) gc.set_conf(my_wxid, "wx_path", wx_path)
set_conf(g.caf, my_wxid, "my_wxid", my_wxid) gc.set_conf(my_wxid, "key", key)
set_conf(g.caf, "auto_setting", "last", my_wxid) gc.set_conf(my_wxid, "my_wxid", my_wxid)
gc.set_conf(gc.at, "last", my_wxid)
rdata = { rdata = {
"merge_path": merge_save_path_new, "merge_path": merge_save_path_new,
"wx_path": wx_path, "wx_path": wx_path,
@ -142,16 +149,22 @@ def init_key():
return ReJson(2001, body=merge_save_path) 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 @error9999
def init_nokey(): def init_nokey(request: InitNoKeyRequest):
""" """
初始化包括key 初始化包括key
:return: :return:
""" """
merge_path = request.json.get("merge_path", "").strip().strip("'").strip('"') merge_path = request.merge_path.strip().strip("'").strip('"')
wx_path = request.json.get("wx_path", "").strip().strip("'").strip('"') wx_path = request.wx_path.strip().strip("'").strip('"')
my_wxid = request.json.get("my_wxid", "").strip().strip("'").strip('"') my_wxid = request.my_wxid.strip().strip("'").strip('"')
if not wx_path: if not wx_path:
return ReJson(1002, body=f"wx_path is required: {wx_path}") return ReJson(1002, body=f"wx_path is required: {wx_path}")
@ -162,18 +175,18 @@ def init_nokey():
if not my_wxid: if not my_wxid:
return ReJson(1002, body=f"my_wxid is required: {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 = { db_config = {
"key": random_str(16), "key": random_str(16),
"type": "sqlite", "type": "sqlite",
"path": merge_path "path": merge_path
} }
set_conf(g.caf, my_wxid, "db_config", db_config) gc.set_conf(my_wxid, "db_config", db_config)
set_conf(g.caf, my_wxid, "merge_path", merge_path) gc.set_conf(my_wxid, "merge_path", merge_path)
set_conf(g.caf, my_wxid, "wx_path", wx_path) gc.set_conf(my_wxid, "wx_path", wx_path)
set_conf(g.caf, my_wxid, "key", key) gc.set_conf(my_wxid, "key", key)
set_conf(g.caf, my_wxid, "my_wxid", my_wxid) gc.set_conf(my_wxid, "my_wxid", my_wxid)
set_conf(g.caf, g.at, "last", my_wxid) gc.set_conf(gc.at, "last", my_wxid)
rdata = { rdata = {
"merge_path": merge_path, "merge_path": merge_path,
"wx_path": wx_path, "wx_path": wx_path,
@ -187,24 +200,24 @@ def init_nokey():
# END 以上为初始化相关 *************************************************************************************************** # END 以上为初始化相关 ***************************************************************************************************
@ls_api.route('/api/ls/realtimemsg', methods=["GET", "POST"]) @ls_api.api_route('/realtimemsg', methods=["GET", "POST"])
@error9999 @error9999
def get_real_time_msg(): def get_real_time_msg():
""" """
获取实时消息 使用 merge_real_time_db()函数 获取实时消息 使用 merge_real_time_db()函数
:return: :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") if not my_wxid: return ReJson(1001, body="my_wxid is required")
merge_path = get_conf(g.caf, my_wxid, "merge_path") merge_path = gc.get_conf(my_wxid, "merge_path")
key = get_conf(g.caf, my_wxid, "key") key = gc.get_conf(my_wxid, "key")
wx_path = get_conf(g.caf, my_wxid, "wx_path") wx_path = gc.get_conf(my_wxid, "wx_path")
if not merge_path or not key or not wx_path or not 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") 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, 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) real_time_exe_path=real_time_exe_path)
@ -216,7 +229,7 @@ def get_real_time_msg():
# start 这部分为专业工具的api ********************************************************************************************* # start 这部分为专业工具的api *********************************************************************************************
@ls_api.route('/api/ls/wxinfo', methods=["GET", 'POST']) @ls_api.api_route('/wxinfo', methods=["GET", 'POST'])
@error9999 @error9999
def get_wxinfo(): def get_wxinfo():
""" """
@ -224,24 +237,33 @@ def get_wxinfo():
:return: :return:
""" """
import pythoncom import pythoncom
pythoncom.CoInitialize() from pywxdump import WX_OFFS
pythoncom.CoInitialize() # 初始化COM库
wxinfos = get_wx_info(WX_OFFS) wxinfos = get_wx_info(WX_OFFS)
pythoncom.CoUninitialize() pythoncom.CoUninitialize() # 释放COM库
return ReJson(0, wxinfos) 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 @error9999
def biasaddr(): def biasaddr(request: BiasAddrRequest):
""" """
BiasAddr BiasAddr
:return: :return:
""" """
mobile = request.json.get("mobile") mobile = request.mobile
name = request.json.get("name") name = request.name
account = request.json.get("account") account = request.account
key = request.json.get("key", "") key = request.json.key
wxdbPath = request.json.get("wxdbPath", "") wxdbPath = request.wxdbPath
if not mobile or not name or not account: if not mobile or not name or not account:
return ReJson(1002) return ReJson(1002)
pythoncom.CoInitialize() pythoncom.CoInitialize()
@ -249,39 +271,33 @@ def biasaddr():
return ReJson(0, str(rdata)) return ReJson(0, str(rdata))
@ls_api.route('/api/ls/decrypt', methods=["GET", 'POST']) @ls_api.api_route('/decrypt', methods=["GET", 'POST'])
@error9999 @error9999
def decrypt(): def decrypt(key: str, wxdbPath: str, outPath: str = ""):
""" """
解密 解密
:return: :return:
""" """
key = request.json.get("key") if not outPath:
if not key: outPath = gc.work_path
return ReJson(1002) wxinfos = batch_decrypt(key, wxdbPath, out_path=outPath)
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)
return ReJson(0, str(wxinfos)) 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 @error9999
def merge(): def merge(request: MergeRequest):
""" """
合并 合并
:return: :return:
""" """
wxdb_path = request.json.get("dbPath") wxdb_path = request.dbPath
if not wxdb_path: out_path = request.outPath
return ReJson(1002)
out_path = request.json.get("outPath")
if not out_path:
return ReJson(1002)
db_path = get_wx_db(wxdb_path) db_path = get_wx_db(wxdb_path)
# for i in db_path:print(i) # for i in db_path:print(i)
rdata = merge_db(db_path, out_path) rdata = merge_db(db_path, out_path)

View File

@ -5,43 +5,38 @@
# Author: xaoyaoo # Author: xaoyaoo
# Date: 2024/01/02 # Date: 2024/01/02
# ------------------------------------------------------------------------------- # -------------------------------------------------------------------------------
import base64
import json
import logging
import os import os
import re
import time import time
import shutil import shutil
from collections import Counter 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 import pywxdump
from pywxdump import decrypt_merge,get_core_db
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.db import DBHandler, download_file, dat2img from pywxdump.db import DBHandler, download_file, dat2img
from pywxdump.db.export import export_csv, export_json, export_html 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 = APIRouter()
rs_api.debug = False
# 是否初始化 # 是否初始化
@rs_api.route('/api/rs/is_init', methods=["GET", 'POST']) @rs_api.api_route('/is_init', methods=["GET", 'POST'])
@error9999 @error9999
def is_init(): def is_init():
""" """
是否初始化 是否初始化
:return: :return:
""" """
local_wxids = get_conf_local_wxid(g.caf)
local_wxids = gc.get_local_wxids()
if len(local_wxids) > 1: if len(local_wxids) > 1:
return ReJson(0, True) return ReJson(0, True)
return ReJson(0, False) return ReJson(0, False)
@ -49,72 +44,62 @@ def is_init():
# start 以下为聊天联系人相关api ******************************************************************************************* # start 以下为聊天联系人相关api *******************************************************************************************
@rs_api.route('/api/rs/mywxid', methods=["GET", 'POST']) @rs_api.api_route('/mywxid', methods=["GET", 'POST'])
@error9999 @error9999
def mywxid(): def mywxid():
""" """
获取我的微信id 获取我的微信id
:return: :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") if not my_wxid: return ReJson(1001, body="my_wxid is required")
return ReJson(0, {"my_wxid": my_wxid}) 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 @error9999
def user_session_list(): def user_session_list():
""" """
获取联系人列表 获取联系人列表
:return: :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") 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) db = DBHandler(db_config, my_wxid=my_wxid)
ret = db.get_session_list() ret = db.get_session_list()
return ReJson(0, list(ret.values())) 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 @error9999
def user_labels_dict(): def user_labels_dict():
""" """
获取标签字典 获取标签字典
:return: :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") 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) db = DBHandler(db_config, my_wxid=my_wxid)
user_labels_dict = db.get_labels() user_labels_dict = db.get_labels()
return ReJson(0, user_labels_dict) return ReJson(0, user_labels_dict)
@rs_api.route('/api/rs/user_list', methods=["GET", 'POST']) @rs_api.post('/user_list')
@error9999 @error9999
def user_list(): def user_list(word: str = "", wxids: List[str] = None, labels: List[str] = None):
""" """
获取联系人列表可用于搜索 获取联系人列表可用于搜索
:return: :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(wxids, str) and wxids == '' or wxids is None: wxids = []
if isinstance(labels, str) and labels == '' or labels is None: labels = [] 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") 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) db = DBHandler(db_config, my_wxid=my_wxid)
users = db.get_user(word, wxids, labels) users = db.get_user(word, wxids, labels)
return ReJson(0, users) return ReJson(0, users)
@ -124,26 +109,72 @@ def user_list():
# start 以下为聊天记录相关api ********************************************************************************************* # start 以下为聊天记录相关api *********************************************************************************************
class MsgCountRequest(BaseModel):
wxids: Optional[List[str]]
@rs_api.route('/api/rs/imgsrc/<path:imgsrc>', methods=["GET", 'POST'])
@rs_api.post('/msg_count')
@error9999 @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. 从网络获取图片主要功能只是下载图片缓存到本地 1. 从网络获取图片主要功能只是下载图片缓存到本地
2. 读取本地图片 2. 读取本地图片
:return: :return:
""" """
imgsrc = src
if not imgsrc: if not imgsrc:
return ReJson(1002) return ReJson(1002)
if imgsrc.startswith("FileStorage"): # 如果是本地图片文件则调用get_img 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") 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_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) original_img_path = os.path.join(wx_path, img_path)
if os.path.exists(original_img_path): if os.path.exists(original_img_path):
rc, fomt, md5, out_bytes = dat2img(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) return ReJson(1001, body=original_img_path)
imgsavepath = os.path.join(str(img_tmp_path), img_path + "_" + "".join([md5, fomt])) imgsavepath = os.path.join(str(img_tmp_path), img_path + "_" + "".join([md5, fomt]))
if os.path.exists(imgsavepath): if os.path.exists(imgsavepath):
return send_file(imgsavepath) return FileResponse(imgsavepath)
if not os.path.exists(os.path.dirname(imgsavepath)): if not os.path.exists(os.path.dirname(imgsavepath)):
os.makedirs(os.path.dirname(imgsavepath)) os.makedirs(os.path.dirname(imgsavepath))
with open(imgsavepath, "wb") as f: with open(imgsavepath, "wb") as f:
f.write(out_bytes) f.write(out_bytes)
return send_file(imgsavepath) return Response(content=out_bytes, media_type="image/jpeg")
else: else:
return ReJson(1001, body=f"{original_img_path} not exists") return ReJson(1001, body=f"{original_img_path} not exists")
elif imgsrc.startswith("http://") or imgsrc.startswith("https://"): elif imgsrc.startswith("http://") or imgsrc.startswith("https://"):
# 将?后面的参数连接到imgsrc # 将?后面的参数连接到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") 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): if not os.path.exists(img_tmp_path):
os.makedirs(img_tmp_path) os.makedirs(img_tmp_path)
file_name = imgsrc.replace("http://", "").replace("https://", "").replace("/", "_").replace("?", "_") 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) img_path_all = os.path.join(str(img_tmp_path), file_name)
if os.path.exists(img_path_all): if os.path.exists(img_path_all):
return send_file(img_path_all) return FileResponse(img_path_all)
else: 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): if os.path.exists(img_path_all):
return send_file(img_path_all) return FileResponse(img_path_all)
else: else:
return ReJson(4004, body=imgsrc) return ReJson(4004, body=imgsrc)
else: else:
return ReJson(1002, body=imgsrc) return ReJson(1002, body=imgsrc)
@rs_api.route('/api/rs/msg_count', methods=["GET", 'POST']) @rs_api.api_route('/video', methods=["GET", 'POST'])
@error9999 def get_video(src: str):
def msg_count():
""" """
获取联系人的聊天记录数量 获取视频
:return: :return:
""" """
if request.method == "GET": videoPath = src
wxid = request.args.get("wxids", []) if not videoPath:
elif request.method == "POST": return ReJson(1002)
wxid = request.json.get("wxids", []) my_wxid = gc.get_conf(gc.at, "last")
else:
return ReJson(1003, msg="Unsupported method")
my_wxid = get_conf(g.caf, g.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required") if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = get_conf(g.caf, my_wxid, "db_config") wx_path = gc.get_conf(my_wxid, "wx_path")
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/<path:videoPath>', 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")
videoPath = videoPath.replace("\\\\", "\\") 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) original_img_path = os.path.join(wx_path, videoPath)
if not os.path.exists(original_img_path): if not os.path.exists(original_img_path):
return ReJson(5002) return ReJson(5002)
# 复制文件到临时文件夹 # 复制文件到临时文件夹
assert isinstance(video_tmp_path, str)
video_save_path = os.path.join(video_tmp_path, videoPath) video_save_path = os.path.join(video_tmp_path, videoPath)
if not os.path.exists(os.path.dirname(video_save_path)): if not os.path.exists(os.path.dirname(video_save_path)):
os.makedirs(os.path.dirname(video_save_path)) os.makedirs(os.path.dirname(video_save_path))
if os.path.exists(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) 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/<path:savePath>', methods=["GET", 'POST']) @rs_api.api_route('/audio', methods=["GET", 'POST'])
def get_audio(savePath): def get_audio(src: str):
my_wxid = get_conf(g.caf, g.at, "last") """
获取语音
: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") 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): 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", "") MsgSvrID = savePath.split("_")[-1].replace(".wav", "")
if not savePath: if not savePath:
@ -282,21 +285,25 @@ def get_audio(savePath):
return ReJson(1001, body="wave_data is required") return ReJson(1001, body="wave_data is required")
if os.path.exists(savePath): if os.path.exists(savePath):
return send_file(savePath) assert isinstance(savePath, str)
return FileResponse(path=savePath, media_type='audio/mpeg')
else: else:
return ReJson(4004, body=savePath) return ReJson(4004, body=savePath)
@rs_api.route('/api/rs/file_info', methods=["GET", 'POST']) class FileInfoRequest(BaseModel):
def get_file_info(): file_path: str
file_path = request.args.get("file_path")
file_path = request.json.get("file_path", file_path)
@rs_api.api_route('/file_info', methods=["GET", 'POST'])
def get_file_info(request: FileInfoRequest):
file_path = request.file_path
if not file_path: if not file_path:
return ReJson(1002) 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") 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) all_file_path = os.path.join(wx_path, file_path)
if not os.path.exists(all_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)}) return ReJson(0, {"file_name": file_name, "file_size": str(file_size)})
@rs_api.route('/api/rs/file/<path:filePath>', methods=["GET", 'POST']) @rs_api.get('/file')
def get_file(filePath): def get_file(src: str):
my_wxid = get_conf(g.caf, g.at, "last") """
获取文件
: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") 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): if not os.path.exists(all_file_path):
return ReJson(5002) 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 ********************************************************************************************* # end 以上为聊天记录相关api *********************************************************************************************
# start 导出聊天记录 ***************************************************************************************************** # 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: :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") 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: 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 ""): if not os.path.exists(wx_path if wx_path else ""):
return ReJson(1002, body=f"wx_path is required: {wx_path}") return ReJson(1002, body=f"wx_path is required: {wx_path}")
@ -342,7 +374,7 @@ def get_export_endb():
if not code: if not code:
return ReJson(2001, body=wxdbpaths) 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): if not os.path.exists(outpath):
os.makedirs(outpath) os.makedirs(outpath)
@ -354,25 +386,28 @@ def get_export_endb():
return ReJson(0, body=outpath) return ReJson(0, body=outpath)
@rs_api.route('/api/rs/export_dedb', methods=["GET", "POST"]) class ExportDedbRequest(BaseModel):
def get_export_dedb(): wx_path: str = ""
outpath: str = ""
key: str = ""
@rs_api.api_route('/export_dedb', methods=["GET", "POST"])
def get_export_dedb(request: ExportDedbRequest):
""" """
导出解密数据库 导出解密数据库
:return: :return:
""" """
if request.method not in ["GET", "POST"]: key = request.key
return ReJson(1003, msg="Unsupported method") wx_path = request.wx_path
rq_data = request.json if request.method == "POST" else request.args
key = rq_data.get("key", "")
wx_path = rq_data.get("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 my_wxid: return ReJson(1001, body="my_wxid is required")
if not key: if not key:
key = get_conf(g.caf, my_wxid, "key") key = gc.get_conf(my_wxid, "key")
if not 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 key: if not key:
return ReJson(1002, body=f"key is required: {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): if not os.path.exists(wx_path):
return ReJson(1001, body=f"wx_path not 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): if not os.path.exists(outpath):
os.makedirs(outpath) os.makedirs(outpath)
assert isinstance(outpath, str) assert isinstance(outpath, str)
@ -393,17 +428,22 @@ def get_export_dedb():
return ReJson(2001, body=merge_save_path) return ReJson(2001, body=merge_save_path)
@rs_api.route('/api/rs/export_csv', methods=["GET", 'POST']) class ExportCsvRequest(BaseModel):
def get_export_csv(): wxid: str
@rs_api.api_route('/export_csv', methods=["GET", 'POST'])
def get_export_csv(request: ExportCsvRequest):
""" """
导出csv 导出csv
:return: :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]) # st_ed_time = request.json.get("datetime", [0, 0])
if not wxid: if not wxid:
return ReJson(1002, body=f"username is required: {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: # if not isinstance(start, int) or not isinstance(end, int) or start >= end:
# return ReJson(1002, body=f"datetime is required: {st_ed_time}") # 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): if not os.path.exists(outpath):
os.makedirs(outpath) os.makedirs(outpath)
@ -424,21 +464,26 @@ def get_export_csv():
return ReJson(2001, body=ret) return ReJson(2001, body=ret)
@rs_api.route('/api/rs/export_json', methods=["GET", 'POST']) class ExportJsonRequest(BaseModel):
def get_export_json(): wxid: str
@rs_api.api_route('/export_json', methods=["GET", 'POST'])
def get_export_json(request: ExportJsonRequest):
""" """
导出json 导出json
:return: :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: if not wxid:
return ReJson(1002, body=f"username is required: {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): if not os.path.exists(outpath):
os.makedirs(outpath) os.makedirs(outpath)
@ -449,21 +494,26 @@ def get_export_json():
return ReJson(2001, body=ret) return ReJson(2001, body=ret)
@rs_api.route('/api/rs/export_html', methods=["GET", 'POST']) class ExportHtmlRequest(BaseModel):
def get_export_html(): wxid: str
@rs_api.api_route('/export_html', methods=["GET", 'POST'])
def get_export_html(request: ExportHtmlRequest):
""" """
导出json 导出json
:return: :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: if not wxid:
return ReJson(1002, body=f"username is required: {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): if not os.path.exists(html_outpath):
os.makedirs(html_outpath) os.makedirs(html_outpath)
assert isinstance(html_outpath, str) 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") web_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "ui", "web")
shutil.copytree(web_path, outpath) shutil.copytree(web_path, outpath)
code, ret = export_html(wxid, outpath, db_config, my_wxid=my_wxid) code, ret = export_html(wxid, outpath, db_config, my_wxid=my_wxid)
if code: if code:
@ -486,69 +535,75 @@ def get_export_html():
# end 导出聊天记录 ******************************************************************************************************* # end 导出聊天记录 *******************************************************************************************************
# start 聊天记录分析api ************************************************************************************************** # 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: :return:
""" """
if request.method not in ["GET", "POST"]: wxid = request.wxid
return ReJson(1003, msg="Unsupported method") start_time = request.start_time
rq_data = request.json if request.method == "POST" else request.args end_time = request.end_time
word = rq_data.get("wxid", "") time_format = request.time_format
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")
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 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) 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) return ReJson(0, date_count)
@rs_api.route('/api/rs/top_talker_count', methods=["GET", 'POST']) class TopTalkerCountRequest(BaseModel):
def get_top_talker_count(): 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: :return:
""" """
if request.method not in ["GET", "POST"]: top = request.top
return ReJson(1003, msg="Unsupported method") start_time = request.start_time
rq_data = request.json if request.method == "POST" else request.args end_time = request.end_time
top = rq_data.get("top", 10)
start_time = rq_data.get("start_time", 0)
end_time = rq_data.get("end_time", 0)
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 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, date_count = DBHandler(db_config, my_wxid=my_wxid).get_top_talker_count(top=top, start_time=start_time,
end_time=end_time) end_time=end_time)
return ReJson(0, date_count) return ReJson(0, date_count)
@rs_api.route('/api/rs/wordcloud', methods=["GET", 'POST']) class WordCloudRequest(BaseModel):
@error9999 target: str = "signature"
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
@rs_api.api_route('/wordcloud', methods=["GET", 'POST'])
@error9999
def get_wordcloud(request: WordCloudRequest):
try: try:
import jieba import jieba
except ImportError: except ImportError:
return ReJson(9999, body="jieba is required") return ReJson(9999, body="jieba is required")
target = rq_data.get("target", "") target = request.target
if not target: if not target:
return ReJson(1002, body="target is required") 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") 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) db = DBHandler(db_config, my_wxid=my_wxid)
if target == "signature": if target == "signature":
@ -583,7 +638,7 @@ def wordcloud():
# end 聊天记录分析api **************************************************************************************************** # end 聊天记录分析api ****************************************************************************************************
# 关于、帮助、设置 ******************************************************************************************************* # 关于、帮助、设置 *******************************************************************************************************
@rs_api.route('/api/rs/check_update', methods=["GET", 'POST']) @rs_api.api_route('/check_update', methods=["GET", 'POST'])
@error9999 @error9999
def check_update(): def check_update():
""" """
@ -609,7 +664,7 @@ def check_update():
return ReJson(9999, msg=str(e)) return ReJson(9999, msg=str(e))
@rs_api.route('/api/rs/version', methods=["GET", 'POST']) @rs_api.api_route('/version', methods=["GET", "POST"])
@error9999 @error9999
def version(): def version():
""" """
@ -619,7 +674,7 @@ def version():
return ReJson(0, pywxdump.__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 @error9999
def get_readme(): def get_readme():
""" """
@ -638,9 +693,3 @@ def get_readme():
# END 关于、帮助、设置 *************************************************************************************************** # END 关于、帮助、设置 ***************************************************************************************************
@rs_api.route('/')
@error9999
def index():
return render_template('index.html')

View File

@ -16,8 +16,122 @@ from .rjson import ReJson
from functools import wraps from functools import wraps
import logging import logging
rs_loger = logging.getLogger("rs_api") server_loger = logging.getLogger("server")
ls_loger = logging.getLogger("ls_api") 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): def get_conf_local_wxid(conf_file):
@ -83,6 +197,16 @@ def set_conf(conf_file, wxid, arg, value):
return True 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): def validate_title(title):
""" """
校验文件名是否合法 校验文件名是否合法
@ -106,6 +230,20 @@ def error9999(func):
return wrapper 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): def gen_base64(path):
# 获取文件名后缀 # 获取文件名后缀
extension = os.path.splitext(path)[1] extension = os.path.splitext(path)[1]

View File

@ -6,8 +6,8 @@
# Date: 2023/10/14 # Date: 2023/10/14
# ------------------------------------------------------------------------------- # -------------------------------------------------------------------------------
import argparse import argparse
import os
import sys import sys
import time
from pywxdump import * from pywxdump import *
import pywxdump import pywxdump
@ -289,7 +289,7 @@ class MainShowChatRecords(BaseSubMainClass):
print("[-] 输入数据库路径不存在") print("[-] 输入数据库路径不存在")
return 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): class MainExportChatRecords(BaseSubMainClass):
@ -327,7 +327,7 @@ class MainUi(BaseSubMainClass):
parser.add_argument("-p", '--port', metavar="", type=int, help="(可选)端口号", default=5000) parser.add_argument("-p", '--port', metavar="", type=int, help="(可选)端口号", default=5000)
parser.add_argument("--online", help="(可选)是否在线查看(局域网查看)", default=False, action='store_true') parser.add_argument("--online", help="(可选)是否在线查看(局域网查看)", default=False, action='store_true')
parser.add_argument("--debug", help="(可选)是否开启debug模式", 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="(可选)用于禁用自动打开浏览器") help="(可选)用于禁用自动打开浏览器")
return parser return parser
@ -339,7 +339,7 @@ class MainUi(BaseSubMainClass):
debug = args.debug debug = args.debug
isopenBrowser = args.isOpenBrowser 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): class MainApi(BaseSubMainClass):
@ -360,7 +360,7 @@ class MainApi(BaseSubMainClass):
port = args.port port = args.port
debug = args.debug 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(): def console_run():

View File

@ -242,7 +242,7 @@ class MsgHandler(DatabaseBase):
voicelength = int(voicelength) / 1000 voicelength = int(voicelength) / 1000
voicelength = f"{voicelength:.2f}" voicelength = f"{voicelength:.2f}"
msg = f"语音时长:{voicelength}\n翻译结果:{transtext}" if transtext else f"语音时长:{voicelength}" 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") f"{CreateTime.replace(':', '-').replace(' ', '_')}_{IsSender}_{MsgSvrID}.wav")
elif type_id == (43, 0): # 视频 elif type_id == (43, 0): # 视频
DictExtra = get_BytesExtra(BytesExtra) DictExtra = get_BytesExtra(BytesExtra)

View File

@ -96,8 +96,11 @@ class DatabaseBase(DatabaseSingletonBase):
def __get_existed_tables(self): def __get_existed_tables(self):
sql = "SELECT tbl_name FROM sqlite_master WHERE type = 'table' and tbl_name!='sqlite_sequence';" sql = "SELECT tbl_name FROM sqlite_master WHERE type = 'table' and tbl_name!='sqlite_sequence';"
existing_tables = self.execute(sql) existing_tables = self.execute(sql)
self.existed_tables = [row[0].lower() for row in existing_tables] if existing_tables:
return self.existed_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): def tables_exist(self, required_tables: str or list):
""" """

View File

@ -252,18 +252,19 @@ def xml2dict(xml_string):
return parse_xml(root) return parse_xml(root)
def download_file(url, save_path=None): def download_file(url, save_path=None, proxies=None):
""" """
下载文件 下载文件
:param url: 文件下载地址 :param url: 文件下载地址
:param save_path: 保存路径 :param save_path: 保存路径
:param proxies: requests 代理
:return: 保存路径 :return: 保存路径
""" """
headers = { 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" "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: if r.status_code != 200:
return None return None
data = r.content data = r.content

View File

@ -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)

View File

@ -15,3 +15,7 @@ lz4
lxml lxml
flask_cors flask_cors
pandas pandas
fastapi
uvicorn
dotenv