解锁并提取Linux客户端微信数据库 (vibe coded)
at 226 lines 8.3 kB view raw
1# -*- coding: utf-8 -*-# 2# ------------------------------------------------------------------------------- 3# Name: messages.py 4# Description: 消息查询与媒体服务 5# ------------------------------------------------------------------------------- 6import os 7import shutil 8from urllib.parse import quote, unquote 9from typing import List, Optional 10 11from fastapi import APIRouter, Response, Body, Request 12from starlette.responses import StreamingResponse, FileResponse 13 14from wxdump_linux.db import DBHandler 15from wxdump_linux.db.utils import download_file, dat2img 16from ..response import ReJson 17from ..decorators import error9999, asyncError9999 18from ..config import gc 19 20messages_router = APIRouter() 21 22 23@messages_router.post('/msg_count') 24@error9999 25def msg_count(wxids: Optional[List[str]] = Body(..., embed=True)): 26 """ 27 获取联系人的聊天记录数量 28 :return: 29 """ 30 my_wxid = gc.get_conf(gc.at, "last") 31 if not my_wxid: return ReJson(1001, body="my_wxid is required") 32 db_config = gc.get_db_config() 33 db = DBHandler(db_config, my_wxid=my_wxid) 34 count = db.get_msgs_count(wxids) 35 return ReJson(0, count) 36 37 38@messages_router.api_route('/msg_list', methods=["GET", 'POST']) 39@error9999 40def get_msgs(wxid: str = Body(...), start: int = Body(...), limit: int = Body(...)): 41 """ 42 获取联系人的聊天记录 43 :return: 44 """ 45 my_wxid = gc.get_conf(gc.at, "last") 46 if not my_wxid: return ReJson(1001, body="my_wxid is required") 47 db_config = gc.get_conf(my_wxid, "db_config") 48 49 db = DBHandler(db_config, my_wxid=my_wxid) 50 msgs, users = db.get_msgs(wxids=wxid, start_index=start, page_size=limit) 51 return ReJson(0, {"msg_list": msgs, "user_list": users}) 52 53 54@messages_router.get('/imgsrc') 55@asyncError9999 56async def get_imgsrc(request: Request): 57 """ 58 获取图片, 59 1. 从网络获取图片,主要功能只是下载图片,缓存到本地 60 2. 读取本地图片 61 :return: 62 """ 63 imgsrc = unquote(str(request.query_params).replace("src=", "", 1)) 64 if not imgsrc: 65 return ReJson(1002) 66 if imgsrc.startswith("FileStorage"): # 如果是本地图片文件则调用get_img 67 my_wxid = gc.get_conf(gc.at, "last") 68 if not my_wxid: return ReJson(1001, body="my_wxid is required") 69 wx_path = gc.get_conf(my_wxid, "wx_path") 70 71 img_path = imgsrc.replace("\\\\", "\\") 72 73 img_tmp_path = os.path.join(gc.work_path, my_wxid, "img") 74 original_img_path = os.path.join(wx_path, img_path) 75 if os.path.exists(original_img_path): 76 rc, fomt, md5, out_bytes = dat2img(original_img_path) 77 if not rc: 78 return ReJson(1001, body=original_img_path) 79 imgsavepath = os.path.join(str(img_tmp_path), img_path + "_" + "".join([md5, fomt])) 80 if os.path.exists(imgsavepath): 81 return FileResponse(imgsavepath) 82 if not os.path.exists(os.path.dirname(imgsavepath)): 83 os.makedirs(os.path.dirname(imgsavepath)) 84 with open(imgsavepath, "wb") as f: 85 f.write(out_bytes) 86 return Response(content=out_bytes, media_type="image/jpeg") 87 else: 88 return ReJson(1001, body=f"{original_img_path} not exists") 89 elif imgsrc.startswith("http://") or imgsrc.startswith("https://"): 90 my_wxid = gc.get_conf(gc.at, "last") 91 if not my_wxid: return ReJson(1001, body="my_wxid is required") 92 93 img_tmp_path = os.path.join(gc.work_path, my_wxid, "imgsrc") 94 if not os.path.exists(img_tmp_path): 95 os.makedirs(img_tmp_path) 96 file_name = imgsrc.replace("http://", "").replace("https://", "").replace("/", "_").replace("?", "_") 97 file_name = file_name + ".jpg" 98 if len(file_name) > 255: 99 file_name = file_name[:255] + "/" + file_name[255:] 100 101 img_path_all = os.path.join(str(img_tmp_path), file_name) 102 if os.path.exists(img_path_all): 103 return FileResponse(img_path_all) 104 else: 105 proxies = None 106 download_file(imgsrc, img_path_all, proxies=proxies) 107 if os.path.exists(img_path_all): 108 return FileResponse(img_path_all) 109 else: 110 return ReJson(4004, body=imgsrc) 111 else: 112 return ReJson(1002, body=imgsrc) 113 114 115@messages_router.api_route('/video', methods=["GET", 'POST']) 116def get_video(request: Request): 117 """ 118 获取视频 119 :return: 120 """ 121 videoPath = unquote(str(request.query_params).replace("src=", "", 1)) 122 if not videoPath: 123 return ReJson(1002) 124 my_wxid = gc.get_conf(gc.at, "last") 125 if not my_wxid: return ReJson(1001, body="my_wxid is required") 126 wx_path = gc.get_conf(my_wxid, "wx_path") 127 128 videoPath = videoPath.replace("\\\\", "\\") 129 130 video_tmp_path = os.path.join(gc.work_path, my_wxid, "video") 131 original_img_path = os.path.join(wx_path, videoPath) 132 if not os.path.exists(original_img_path): 133 return ReJson(5002) 134 assert isinstance(video_tmp_path, str) 135 video_save_path = os.path.join(video_tmp_path, videoPath) 136 if not os.path.exists(os.path.dirname(video_save_path)): 137 os.makedirs(os.path.dirname(video_save_path)) 138 if os.path.exists(video_save_path): 139 return FileResponse(path=video_save_path) 140 shutil.copy(original_img_path, video_save_path) 141 return FileResponse(path=video_save_path) 142 143 144@messages_router.api_route('/audio', methods=["GET", 'POST']) 145def get_audio(request: Request): 146 """ 147 获取语音 148 :return: 149 """ 150 savePath = unquote(str(request.query_params).replace("src=", "", 1)).replace("audio\\", "", 1) 151 if not savePath: 152 return ReJson(1002) 153 my_wxid = gc.get_conf(gc.at, "last") 154 if not my_wxid: return ReJson(1001, body="my_wxid is required") 155 db_config = gc.get_conf(my_wxid, "db_config") 156 157 savePath = os.path.join(gc.work_path, my_wxid, "audio", savePath) 158 if os.path.exists(savePath): 159 assert isinstance(savePath, str) 160 return FileResponse(path=savePath, media_type='audio/mpeg') 161 162 MsgSvrID = savePath.split("_")[-1].replace(".wav", "") 163 if not savePath: 164 return ReJson(1002) 165 166 if not os.path.exists(os.path.dirname(savePath)): 167 os.makedirs(os.path.dirname(savePath)) 168 169 db = DBHandler(db_config, my_wxid=my_wxid) 170 wave_data = db.get_audio(MsgSvrID, is_play=False, is_wave=True, save_path=savePath, rate=24000) 171 if not wave_data: 172 return ReJson(1001, body="wave_data is required") 173 174 if os.path.exists(savePath): 175 assert isinstance(savePath, str) 176 return FileResponse(path=savePath, media_type='audio/mpeg') 177 else: 178 return ReJson(4004, body=savePath) 179 180 181@messages_router.api_route('/file_info', methods=["GET", 'POST']) 182def get_file_info(file_path: str = Body(..., embed=True)): 183 if not file_path: 184 return ReJson(1002) 185 186 my_wxid = gc.get_conf(gc.at, "last") 187 if not my_wxid: return ReJson(1001, body="my_wxid is required") 188 wx_path = gc.get_conf(my_wxid, "wx_path") 189 190 all_file_path = os.path.join(wx_path, file_path) 191 if not os.path.exists(all_file_path): 192 return ReJson(5002) 193 file_name = os.path.basename(all_file_path) 194 file_size = os.path.getsize(all_file_path) 195 return ReJson(0, {"file_name": file_name, "file_size": str(file_size)}) 196 197 198@messages_router.get('/file') 199def get_file(request: Request): 200 """ 201 获取文件 202 :return: 203 """ 204 file_path = unquote(str(request.query_params).replace("src=", "", 1)) 205 if not file_path: 206 return ReJson(1002) 207 my_wxid = gc.get_conf(gc.at, "last") 208 if not my_wxid: return ReJson(1001, body="my_wxid is required") 209 wx_path = gc.get_conf(my_wxid, "wx_path") 210 211 all_file_path = os.path.join(wx_path, file_path) 212 if not os.path.exists(all_file_path): 213 return ReJson(5002) 214 215 def file_iterator(file_path, chunk_size=8192): 216 with open(file_path, "rb") as f: 217 while True: 218 chunk = f.read(chunk_size) 219 if not chunk: 220 break 221 yield chunk 222 223 headers = { 224 "Content-Disposition": f'attachment; filename*=UTF-8\'\'{quote(os.path.basename(all_file_path))}', 225 } 226 return StreamingResponse(file_iterator(all_file_path), media_type="application/octet-stream", headers=headers)