mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
fix(chat-media): 修复聊天图片缓存降级并刷新当前会话媒体
- 优先使用更高质量的微信图片资源并回写本地缓存 - 图片接口返回 no-store,避免浏览器继续命中旧缓存 - 页面恢复前台时刷新当前会话媒体资源,并补充相关测试
This commit is contained in:
@@ -74,6 +74,7 @@ export const useChatMessages = ({
|
||||
let highlightTimer = null
|
||||
|
||||
const messageTypeFilter = ref('all')
|
||||
const localMediaVersion = ref(0)
|
||||
const messageTypeFilterOptions = [
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'text', label: '文本' },
|
||||
@@ -95,9 +96,39 @@ export const useChatMessages = ({
|
||||
const normalizeMessage = createMessageNormalizer({
|
||||
apiBase,
|
||||
getSelectedAccount: () => selectedAccount.value,
|
||||
getSelectedContact: () => selectedContact.value
|
||||
getSelectedContact: () => selectedContact.value,
|
||||
getLocalMediaVersion: () => localMediaVersion.value
|
||||
})
|
||||
|
||||
const bumpLocalMediaVersion = () => {
|
||||
localMediaVersion.value = (localMediaVersion.value + 1) % 1000000000
|
||||
return localMediaVersion.value
|
||||
}
|
||||
|
||||
const renormalizeLoadedMessages = (username) => {
|
||||
const key = String(username || '').trim()
|
||||
if (!key) return
|
||||
const existing = allMessages.value[key]
|
||||
if (!Array.isArray(existing) || !existing.length) return
|
||||
|
||||
const refreshed = dedupeMessagesById(existing.map((message) => {
|
||||
const normalized = normalizeMessage(message)
|
||||
return {
|
||||
...message,
|
||||
...normalized,
|
||||
_emojiDownloading: !!message?._emojiDownloading,
|
||||
_emojiDownloaded: typeof message?._emojiDownloaded === 'boolean' ? message._emojiDownloaded : normalized._emojiDownloaded,
|
||||
_quoteImageError: false,
|
||||
_quoteThumbError: false
|
||||
}
|
||||
}))
|
||||
|
||||
allMessages.value = {
|
||||
...allMessages.value,
|
||||
[key]: refreshed
|
||||
}
|
||||
}
|
||||
|
||||
const messages = computed(() => {
|
||||
if (!selectedContact.value) return []
|
||||
return allMessages.value[selectedContact.value.username] || []
|
||||
@@ -534,9 +565,17 @@ export const useChatMessages = ({
|
||||
|
||||
const refreshSelectedMessages = async () => {
|
||||
if (!selectedContact.value) return
|
||||
bumpLocalMediaVersion()
|
||||
await loadMessages({ username: selectedContact.value.username, reset: true })
|
||||
}
|
||||
|
||||
const refreshCurrentMessageMedia = async () => {
|
||||
if (!selectedContact.value?.username) return
|
||||
bumpLocalMediaVersion()
|
||||
renormalizeLoadedMessages(selectedContact.value.username)
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const refreshRealtimeIncremental = async () => {
|
||||
if (!realtimeEnabled.value || !selectedAccount.value || !selectedContact.value?.username) return
|
||||
if (searchContext.value?.active || isLoadingMessages.value) return
|
||||
@@ -912,6 +951,7 @@ export const useChatMessages = ({
|
||||
loadMessages,
|
||||
loadMoreMessages,
|
||||
refreshSelectedMessages,
|
||||
refreshCurrentMessageMedia,
|
||||
refreshRealtimeIncremental,
|
||||
queueRealtimeRefresh,
|
||||
tryEnableRealtimeAuto,
|
||||
|
||||
@@ -17,11 +17,12 @@ const buildAccountMediaUrl = (apiBase, path, parts) => {
|
||||
return `${apiBase}${path}?${parts.filter(Boolean).join('&')}`
|
||||
}
|
||||
|
||||
export const createMessageNormalizer = ({ apiBase, getSelectedAccount, getSelectedContact }) => {
|
||||
export const createMessageNormalizer = ({ apiBase, getSelectedAccount, getSelectedContact, getLocalMediaVersion }) => {
|
||||
return (msg) => {
|
||||
const account = String(getSelectedAccount?.() || '').trim()
|
||||
const contact = getSelectedContact?.() || null
|
||||
const username = String(contact?.username || '').trim()
|
||||
const localMediaVersion = Number(getLocalMediaVersion?.() || 0)
|
||||
const isSent = !!msg.isSent
|
||||
const sender = isSent ? '我' : (msg.senderDisplayName || msg.senderUsername || contact?.name || '')
|
||||
const fallbackAvatar = (!isSent && !contact?.isGroup) ? (contact?.avatar || null) : null
|
||||
@@ -66,7 +67,8 @@ export const createMessageNormalizer = ({ apiBase, getSelectedAccount, getSelect
|
||||
`account=${encodeURIComponent(account)}`,
|
||||
msg.imageMd5 ? `md5=${encodeURIComponent(msg.imageMd5)}` : '',
|
||||
msg.imageFileId ? `file_id=${encodeURIComponent(msg.imageFileId)}` : '',
|
||||
`username=${encodeURIComponent(username)}`
|
||||
`username=${encodeURIComponent(username)}`,
|
||||
localMediaVersion > 0 ? `v=${encodeURIComponent(String(localMediaVersion))}` : ''
|
||||
])
|
||||
})()
|
||||
|
||||
@@ -86,7 +88,8 @@ export const createMessageNormalizer = ({ apiBase, getSelectedAccount, getSelect
|
||||
`account=${encodeURIComponent(account)}`,
|
||||
msg.videoThumbMd5 ? `md5=${encodeURIComponent(msg.videoThumbMd5)}` : '',
|
||||
msg.videoThumbFileId ? `file_id=${encodeURIComponent(msg.videoThumbFileId)}` : '',
|
||||
`username=${encodeURIComponent(username)}`
|
||||
`username=${encodeURIComponent(username)}`,
|
||||
localMediaVersion > 0 ? `v=${encodeURIComponent(String(localMediaVersion))}` : ''
|
||||
])
|
||||
})()
|
||||
|
||||
@@ -158,7 +161,8 @@ export const createMessageNormalizer = ({ apiBase, getSelectedAccount, getSelect
|
||||
return buildAccountMediaUrl(apiBase, '/chat/media/image', [
|
||||
`account=${encodeURIComponent(account)}`,
|
||||
`server_id=${encodeURIComponent(quoteServerIdStr)}`,
|
||||
username ? `username=${encodeURIComponent(username)}` : ''
|
||||
username ? `username=${encodeURIComponent(username)}` : '',
|
||||
localMediaVersion > 0 ? `v=${encodeURIComponent(String(localMediaVersion))}` : ''
|
||||
])
|
||||
})()
|
||||
|
||||
|
||||
@@ -216,6 +216,7 @@ const {
|
||||
loadMessages,
|
||||
loadMoreMessages,
|
||||
refreshSelectedMessages,
|
||||
refreshCurrentMessageMedia,
|
||||
queueRealtimeRefresh,
|
||||
tryEnableRealtimeAuto,
|
||||
resetMessageState,
|
||||
@@ -568,6 +569,28 @@ const onGlobalKeyDown = (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
let lastResumeMediaRefreshAt = 0
|
||||
|
||||
const maybeRefreshMediaOnResume = () => {
|
||||
if (!process.client) return
|
||||
if (!selectedContact.value?.username) return
|
||||
if (searchContext.value?.active) return
|
||||
|
||||
const now = Date.now()
|
||||
if ((now - lastResumeMediaRefreshAt) < 1200) return
|
||||
lastResumeMediaRefreshAt = now
|
||||
void refreshCurrentMessageMedia()
|
||||
}
|
||||
|
||||
const onWindowFocus = () => {
|
||||
maybeRefreshMediaOnResume()
|
||||
}
|
||||
|
||||
const onVisibilityChange = () => {
|
||||
if (document.visibilityState !== 'visible') return
|
||||
maybeRefreshMediaOnResume()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!process.client) return
|
||||
|
||||
@@ -585,6 +608,8 @@ onMounted(async () => {
|
||||
document.addEventListener('touchmove', onFloatingWindowMouseMove)
|
||||
document.addEventListener('touchend', onFloatingWindowMouseUp)
|
||||
document.addEventListener('touchcancel', onFloatingWindowMouseUp)
|
||||
window.addEventListener('focus', onWindowFocus)
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
|
||||
logChatBootstrap('loadContacts:start', {
|
||||
selectedAccount: selectedAccount.value
|
||||
@@ -635,6 +660,8 @@ onUnmounted(() => {
|
||||
document.removeEventListener('touchmove', onFloatingWindowMouseMove)
|
||||
document.removeEventListener('touchend', onFloatingWindowMouseUp)
|
||||
document.removeEventListener('touchcancel', onFloatingWindowMouseUp)
|
||||
window.removeEventListener('focus', onWindowFocus)
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
|
||||
if (locateServerIdTimer) clearTimeout(locateServerIdTimer)
|
||||
locateServerIdTimer = null
|
||||
|
||||
@@ -67,6 +67,87 @@ logger = get_logger(__name__)
|
||||
router = APIRouter(route_class=PathFixRoute)
|
||||
|
||||
|
||||
def _build_uncached_media_response(data: bytes, media_type: str) -> Response:
|
||||
resp = Response(content=data, media_type=media_type)
|
||||
resp.headers["Cache-Control"] = "no-store"
|
||||
return resp
|
||||
|
||||
|
||||
def _image_candidate_variant_rank(path: Path) -> int:
|
||||
stem = str(path.stem or "").lower()
|
||||
if stem.endswith(("_b", ".b")):
|
||||
return 0
|
||||
if stem.endswith(("_h", ".h")):
|
||||
return 1
|
||||
if stem.endswith(("_c", ".c")):
|
||||
return 3
|
||||
if stem.endswith(("_t", ".t")):
|
||||
return 4
|
||||
return 2
|
||||
|
||||
|
||||
def _image_candidate_stat(path: Optional[Path]) -> tuple[int, float]:
|
||||
if not path:
|
||||
return 0, 0.0
|
||||
try:
|
||||
st = path.stat()
|
||||
return int(st.st_size), float(st.st_mtime)
|
||||
except Exception:
|
||||
return 0, 0.0
|
||||
|
||||
|
||||
def _should_prefer_live_image_candidates(
|
||||
*,
|
||||
cached_path: Optional[Path],
|
||||
live_candidates: list[Path],
|
||||
) -> bool:
|
||||
if not live_candidates:
|
||||
return False
|
||||
if not cached_path:
|
||||
return True
|
||||
|
||||
best_live = live_candidates[0]
|
||||
live_rank = _image_candidate_variant_rank(best_live)
|
||||
if live_rank < 2:
|
||||
return True
|
||||
|
||||
cache_size, cache_mtime = _image_candidate_stat(cached_path)
|
||||
live_size, live_mtime = _image_candidate_stat(best_live)
|
||||
if live_rank == 2 and live_size > cache_size:
|
||||
return True
|
||||
if live_rank == 2 and live_size >= cache_size and live_mtime > cache_mtime:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _write_cached_chat_image(account_dir: Path, md5: str, data: bytes) -> None:
|
||||
md5_norm = str(md5 or "").strip().lower()
|
||||
if (not md5_norm) or (not data):
|
||||
return
|
||||
|
||||
ext = _detect_image_extension(data)
|
||||
out_path = _get_decrypted_resource_path(account_dir, md5_norm, ext)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for stale_ext in ("jpg", "png", "gif", "webp", "dat"):
|
||||
stale_path = _get_decrypted_resource_path(account_dir, md5_norm, stale_ext)
|
||||
if stale_path == out_path:
|
||||
continue
|
||||
try:
|
||||
if stale_path.exists():
|
||||
stale_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if out_path.exists() and out_path.read_bytes() == data:
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
out_path.write_bytes(data)
|
||||
|
||||
|
||||
def _resolve_avatar_remote_url(*, account_dir: Path, username: str) -> str:
|
||||
u = str(username or "").strip()
|
||||
if not u:
|
||||
@@ -1311,20 +1392,26 @@ async def get_chat_image(
|
||||
if md5_from_msg:
|
||||
md5 = md5_from_msg
|
||||
|
||||
# md5 模式:优先从解密资源目录读取(更快)
|
||||
cached_path: Optional[Path] = None
|
||||
cached_data = b""
|
||||
cached_media_type = "application/octet-stream"
|
||||
|
||||
# md5 模式:优先检查解密资源目录;如果微信目录里已经有更高质量版本,会在后面自动升级。
|
||||
if md5:
|
||||
decrypted_path = _try_find_decrypted_resource(account_dir, str(md5).lower())
|
||||
if decrypted_path:
|
||||
data = decrypted_path.read_bytes()
|
||||
media_type = _detect_image_media_type(data[:32])
|
||||
if media_type != "application/octet-stream" and _is_probably_valid_image(data, media_type):
|
||||
return Response(content=data, media_type=media_type)
|
||||
cached_path = decrypted_path
|
||||
cached_data = data
|
||||
cached_media_type = media_type
|
||||
# Corrupted cached file (e.g. wrong ext / partial data): remove and regenerate from source.
|
||||
try:
|
||||
if decrypted_path.suffix.lower() in {".jpg", ".jpeg", ".png", ".gif", ".webp"}:
|
||||
elif decrypted_path.suffix.lower() in {".jpg", ".jpeg", ".png", ".gif", ".webp"}:
|
||||
try:
|
||||
decrypted_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 回退:从微信数据目录实时定位并解密
|
||||
wxid_dir = _resolve_account_wxid_dir(account_dir)
|
||||
@@ -1414,11 +1501,36 @@ async def get_chat_image(
|
||||
break
|
||||
|
||||
if not p:
|
||||
if cached_path:
|
||||
return _build_uncached_media_response(cached_data, cached_media_type)
|
||||
raise HTTPException(status_code=404, detail="Image not found.")
|
||||
|
||||
candidates.extend(_iter_media_source_candidates(p))
|
||||
candidates = _order_media_candidates(candidates)
|
||||
|
||||
if cached_path:
|
||||
try:
|
||||
cached_key = str(cached_path.resolve())
|
||||
except Exception:
|
||||
cached_key = str(cached_path)
|
||||
|
||||
live_candidates: list[Path] = []
|
||||
seen_live: set[str] = set()
|
||||
for candidate in candidates:
|
||||
try:
|
||||
key = str(candidate.resolve())
|
||||
except Exception:
|
||||
key = str(candidate)
|
||||
if key == cached_key or key in seen_live:
|
||||
continue
|
||||
seen_live.add(key)
|
||||
live_candidates.append(candidate)
|
||||
|
||||
if _should_prefer_live_image_candidates(cached_path=cached_path, live_candidates=live_candidates):
|
||||
candidates = [*live_candidates, cached_path]
|
||||
else:
|
||||
candidates = [cached_path, *live_candidates]
|
||||
|
||||
logger.info(f"chat_image: md5={md5} file_id={file_id} candidates={len(candidates)} first={p}")
|
||||
|
||||
data = b""
|
||||
@@ -1443,19 +1555,14 @@ async def get_chat_image(
|
||||
# 仅在 md5 有效时缓存到 resource 目录;file_id 可能非常长,避免写入超长文件名
|
||||
if md5 and media_type.startswith("image/"):
|
||||
try:
|
||||
out_md5 = str(md5).lower()
|
||||
ext = _detect_image_extension(data)
|
||||
out_path = _get_decrypted_resource_path(account_dir, out_md5, ext)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not out_path.exists():
|
||||
out_path.write_bytes(data)
|
||||
_write_cached_chat_image(account_dir, str(md5), data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(
|
||||
f"chat_image: md5={md5} file_id={file_id} chosen={chosen} media_type={media_type} bytes={len(data)}"
|
||||
)
|
||||
return Response(content=data, media_type=media_type)
|
||||
return _build_uncached_media_response(data, media_type)
|
||||
|
||||
|
||||
@router.get("/api/chat/media/emoji", summary="获取表情消息资源")
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import hashlib
|
||||
import importlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
|
||||
class TestChatMediaImageCacheUpgrade(unittest.TestCase):
|
||||
def _seed_contact_db(self, path: Path, *, account: str, username: str) -> None:
|
||||
conn = sqlite3.connect(str(path))
|
||||
try:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE contact (
|
||||
username TEXT,
|
||||
remark TEXT,
|
||||
nick_name TEXT,
|
||||
alias TEXT,
|
||||
local_type INTEGER,
|
||||
verify_flag INTEGER,
|
||||
big_head_url TEXT,
|
||||
small_head_url TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE stranger (
|
||||
username TEXT,
|
||||
remark TEXT,
|
||||
nick_name TEXT,
|
||||
alias TEXT,
|
||||
local_type INTEGER,
|
||||
verify_flag INTEGER,
|
||||
big_head_url TEXT,
|
||||
small_head_url TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO contact VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(account, "", "我", "", 1, 0, "", ""),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO contact VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(username, "", "测试好友", "", 1, 0, "", ""),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _seed_session_db(self, path: Path, *, username: str) -> None:
|
||||
conn = sqlite3.connect(str(path))
|
||||
try:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE SessionTable (
|
||||
username TEXT,
|
||||
is_hidden INTEGER,
|
||||
sort_timestamp INTEGER
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("INSERT INTO SessionTable VALUES (?, ?, ?)", (username, 0, 1735689600))
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _seed_source_info(self, account_dir: Path, *, wxid_dir: Path) -> None:
|
||||
payload = {
|
||||
"wxid_dir": str(wxid_dir),
|
||||
"db_storage_path": "",
|
||||
}
|
||||
(account_dir / "_source.json").write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
def _seed_cached_resource(self, account_dir: Path, *, md5: str, payload: bytes) -> Path:
|
||||
resource_dir = account_dir / "resource" / md5[:2]
|
||||
resource_dir.mkdir(parents=True, exist_ok=True)
|
||||
target = resource_dir / f"{md5}.jpg"
|
||||
target.write_bytes(payload)
|
||||
return target
|
||||
|
||||
def _seed_live_variant(self, wxid_dir: Path, *, username: str, md5: str, suffix: str, payload: bytes) -> Path:
|
||||
chat_hash = hashlib.md5(username.encode("utf-8")).hexdigest()
|
||||
target = wxid_dir / "msg" / "attach" / chat_hash / "2026-03" / "Img" / f"{md5}{suffix}.dat"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes(payload)
|
||||
return target
|
||||
|
||||
def _build_client(self):
|
||||
import wechat_decrypt_tool.logging_config as logging_config
|
||||
import wechat_decrypt_tool.app_paths as app_paths
|
||||
import wechat_decrypt_tool.media_helpers as media_helpers
|
||||
import wechat_decrypt_tool.routers.chat_media as chat_media
|
||||
|
||||
logging.shutdown()
|
||||
importlib.reload(logging_config)
|
||||
importlib.reload(app_paths)
|
||||
importlib.reload(media_helpers)
|
||||
importlib.reload(chat_media)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(chat_media.router)
|
||||
return TestClient(app)
|
||||
|
||||
def test_live_high_variant_replaces_stale_cached_thumb(self):
|
||||
with TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
account = "wxid_test"
|
||||
username = "wxid_friend"
|
||||
md5 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
account_dir = root / "output" / "databases" / account
|
||||
wxid_dir = root / "wxid_source"
|
||||
account_dir.mkdir(parents=True, exist_ok=True)
|
||||
wxid_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._seed_contact_db(account_dir / "contact.db", account=account, username=username)
|
||||
self._seed_session_db(account_dir / "session.db", username=username)
|
||||
self._seed_source_info(account_dir, wxid_dir=wxid_dir)
|
||||
|
||||
cached_thumb = b"\xff\xd8\xff\xd9"
|
||||
live_original = b"\xff\xd8\xff\xe0" + (b"\x00" * 48) + b"\xff\xd9"
|
||||
cache_path = self._seed_cached_resource(account_dir, md5=md5, payload=cached_thumb)
|
||||
self._seed_live_variant(wxid_dir, username=username, md5=md5, suffix="_h", payload=live_original)
|
||||
|
||||
prev_data = os.environ.get("WECHAT_TOOL_DATA_DIR")
|
||||
client = None
|
||||
try:
|
||||
os.environ["WECHAT_TOOL_DATA_DIR"] = str(root)
|
||||
client = self._build_client()
|
||||
resp = client.get(
|
||||
"/api/chat/media/image",
|
||||
params={"account": account, "md5": md5, "username": username},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.content, live_original)
|
||||
self.assertEqual(resp.headers.get("cache-control"), "no-store")
|
||||
self.assertEqual(cache_path.read_bytes(), live_original)
|
||||
finally:
|
||||
try:
|
||||
client.close()
|
||||
except Exception:
|
||||
pass
|
||||
logging.shutdown()
|
||||
if prev_data is None:
|
||||
os.environ.pop("WECHAT_TOOL_DATA_DIR", None)
|
||||
else:
|
||||
os.environ["WECHAT_TOOL_DATA_DIR"] = prev_data
|
||||
|
||||
def test_cached_original_is_not_downgraded_by_live_thumb(self):
|
||||
with TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
account = "wxid_test"
|
||||
username = "wxid_friend"
|
||||
md5 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
|
||||
account_dir = root / "output" / "databases" / account
|
||||
wxid_dir = root / "wxid_source"
|
||||
account_dir.mkdir(parents=True, exist_ok=True)
|
||||
wxid_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._seed_contact_db(account_dir / "contact.db", account=account, username=username)
|
||||
self._seed_session_db(account_dir / "session.db", username=username)
|
||||
self._seed_source_info(account_dir, wxid_dir=wxid_dir)
|
||||
|
||||
cached_original = b"\xff\xd8\xff\xe0" + (b"\x11" * 64) + b"\xff\xd9"
|
||||
live_thumb = b"\xff\xd8\xff\xd9"
|
||||
cache_path = self._seed_cached_resource(account_dir, md5=md5, payload=cached_original)
|
||||
self._seed_live_variant(wxid_dir, username=username, md5=md5, suffix="_t", payload=live_thumb)
|
||||
|
||||
prev_data = os.environ.get("WECHAT_TOOL_DATA_DIR")
|
||||
client = None
|
||||
try:
|
||||
os.environ["WECHAT_TOOL_DATA_DIR"] = str(root)
|
||||
client = self._build_client()
|
||||
resp = client.get(
|
||||
"/api/chat/media/image",
|
||||
params={"account": account, "md5": md5, "username": username},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.content, cached_original)
|
||||
self.assertEqual(resp.headers.get("cache-control"), "no-store")
|
||||
self.assertEqual(cache_path.read_bytes(), cached_original)
|
||||
finally:
|
||||
try:
|
||||
client.close()
|
||||
except Exception:
|
||||
pass
|
||||
logging.shutdown()
|
||||
if prev_data is None:
|
||||
os.environ.pop("WECHAT_TOOL_DATA_DIR", None)
|
||||
else:
|
||||
os.environ["WECHAT_TOOL_DATA_DIR"] = prev_data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user