解锁并提取Linux客户端微信数据库 (vibe coded)
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)