mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
Compare commits
3 Commits
@@ -6,6 +6,7 @@
|
||||
<h1>WeChatDataAnalysis - 微信数据库解密与分析工具</h1>
|
||||
<p>微信4.x数据解密并生成年度总结,高仿微信,支持实时更新,导出聊天记录,朋友圈等大量便捷功能</p>
|
||||
<p><b>特别致谢</b>:<a href="https://github.com/H3CoF6">H3CoF6</a>(密钥与朋友圈等核心内容的技术支持)、<a href="https://github.com/ycccccccy/echotrace">echotrace</a>、<a href="https://github.com/hicccc77/WeFlow">WeFlow</a>(本项目大量功能参考其实现)</p>
|
||||
<p>如需定制功能,请联系 QQ:2977094657。</p>
|
||||
<img src="https://img.shields.io/github/v/tag/LifeArchiveProject/WeChatDataAnalysis" alt="Version" />
|
||||
<img src="https://img.shields.io/github/stars/LifeArchiveProject/WeChatDataAnalysis" alt="Stars" />
|
||||
<img src="https://gh-down-badges.linkof.link/LifeArchiveProject/WeChatDataAnalysis" alt="Downloads" />
|
||||
@@ -192,16 +193,6 @@ npm run dist
|
||||
3. **数据隐私**: 解密后的数据包含个人隐私信息,请谨慎处理
|
||||
4. **合法使用**: 请遵守相关法律法规,不得用于非法目的
|
||||
|
||||
## 修改消息
|
||||
|
||||
支持在聊天页对单条消息进行本地修改(如修改消息文本/字段、修复为我发送、反转本地气泡方向),并在“修改记录”页查看原始与当前对比,支持单条恢复或按会话一键恢复。
|
||||
|
||||
该功能只修改本机本地数据库(`db_storage` 与解密副本),不会调用远端回写接口。
|
||||
|
||||
<p align="center">
|
||||
<img src="frontend/public/edit.gif" alt="本地消息修改" width="800" />
|
||||
</p>
|
||||
|
||||
## 致谢
|
||||
|
||||
本项目的开发过程中参考了以下优秀的开源项目和资源:
|
||||
|
||||
@@ -1139,6 +1139,132 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wechat-link-card-finder {
|
||||
width: 135px;
|
||||
min-width: 135px;
|
||||
max-width: 135px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wechat-link-card-finder.wechat-link-card--disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.wechat-link-finder-cover {
|
||||
width: 135px;
|
||||
height: 185px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: var(--app-surface-muted);
|
||||
}
|
||||
|
||||
.wechat-link-finder-cover--empty {
|
||||
background: linear-gradient(180deg, #37cc6a 0%, #118f42 100%);
|
||||
}
|
||||
|
||||
.wechat-link-finder-cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wechat-link-finder-cover-placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.wechat-link-finder-cover-placeholder svg {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.wechat-link-finder-cover-shade {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.12) 42%, rgba(0, 0, 0, 0.68) 100%);
|
||||
}
|
||||
|
||||
.wechat-link-finder-play {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -66%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.42);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.wechat-link-finder-play svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.wechat-link-finder-meta {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.wechat-link-finder-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
padding: 5px 7px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.wechat-link-finder-author-avatar {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wechat-link-finder-author-avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wechat-link-finder-author-name {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.96);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
/* 隐私模式模糊效果 */
|
||||
.privacy-blur {
|
||||
filter: blur(9px);
|
||||
|
||||
@@ -1437,14 +1437,20 @@
|
||||
|
||||
.session-list-item-name {
|
||||
color: var(--session-list-name);
|
||||
font-weight: 400;
|
||||
font-synthesis: none;
|
||||
}
|
||||
|
||||
.session-list-item-time {
|
||||
color: var(--session-list-meta);
|
||||
font-weight: 400;
|
||||
font-synthesis: none;
|
||||
}
|
||||
|
||||
.session-list-item-preview {
|
||||
color: var(--session-list-preview);
|
||||
font-weight: 400;
|
||||
font-synthesis: none;
|
||||
}
|
||||
|
||||
.contact-search-wrapper {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { defineComponent, h, ref, watch } from 'vue'
|
||||
import miniProgramIconUrl from '~/assets/images/wechat/mini-program.svg'
|
||||
|
||||
const finderLogoUrl = '/assets/images/wechat/channels-logo.svg'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LinkCard',
|
||||
props: {
|
||||
@@ -51,7 +53,11 @@ export default defineComponent({
|
||||
return text ? (Array.from(text)[0] || '') : ''
|
||||
})()
|
||||
const fromAvatarUrl = String(props.fromAvatar || '').trim()
|
||||
const headingText = String(props.heading || href || '').trim()
|
||||
let abstractText = String(props.abstract || '').trim()
|
||||
if (abstractText && headingText && abstractText === headingText) abstractText = ''
|
||||
const isMiniProgram = String(props.linkType || '').trim() === 'mini_program'
|
||||
const isFinder = String(props.linkType || '').trim() === 'finder'
|
||||
const isCoverVariant = !isMiniProgram && String(props.variant || '').trim() === 'cover'
|
||||
const Tag = canNavigate ? 'a' : 'div'
|
||||
|
||||
@@ -140,9 +146,68 @@ export default defineComponent({
|
||||
)
|
||||
}
|
||||
|
||||
const headingText = String(props.heading || href || '').trim()
|
||||
let abstractText = String(props.abstract || '').trim()
|
||||
if (abstractText && headingText && abstractText === headingText) abstractText = ''
|
||||
if (isFinder) {
|
||||
return h(
|
||||
Tag,
|
||||
{
|
||||
...(canNavigate ? { href, target: '_blank', rel: 'noreferrer' } : { role: 'group', 'aria-disabled': 'true' }),
|
||||
class: [
|
||||
'wechat-link-card-finder',
|
||||
!canNavigate ? 'wechat-link-card--disabled' : '',
|
||||
'wechat-special-card',
|
||||
'msg-radius',
|
||||
props.isSent ? 'wechat-special-sent-side' : ''
|
||||
].filter(Boolean).join(' '),
|
||||
style: {
|
||||
width: '135px',
|
||||
minWidth: '135px',
|
||||
maxWidth: '135px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxSizing: 'border-box',
|
||||
flex: '0 0 auto',
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
textDecoration: 'none',
|
||||
outline: 'none'
|
||||
}
|
||||
},
|
||||
[
|
||||
h('div', { class: ['wechat-link-finder-cover', !props.preview ? 'wechat-link-finder-cover--empty' : ''].filter(Boolean).join(' ') }, [
|
||||
props.preview
|
||||
? h('img', {
|
||||
src: props.preview,
|
||||
alt: props.heading || '视频号封面',
|
||||
class: 'wechat-link-finder-cover-img',
|
||||
referrerpolicy: 'no-referrer'
|
||||
})
|
||||
: h('div', { class: 'wechat-link-finder-cover-placeholder', 'aria-hidden': 'true' }, [
|
||||
h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [
|
||||
h('path', { d: 'M8 5v14l11-7z' })
|
||||
])
|
||||
]),
|
||||
h('div', { class: 'wechat-link-finder-cover-shade', 'aria-hidden': 'true' }),
|
||||
h('div', { class: 'wechat-link-finder-play', 'aria-hidden': 'true' }, [
|
||||
h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [
|
||||
h('path', { d: 'M8 5v14l11-7z' })
|
||||
])
|
||||
]),
|
||||
h('div', { class: 'wechat-link-finder-meta' }, [
|
||||
h('div', { class: 'wechat-link-finder-author' }, [
|
||||
h('div', { class: 'wechat-link-finder-author-avatar', 'aria-hidden': 'true' }, [
|
||||
h('img', {
|
||||
src: finderLogoUrl,
|
||||
alt: '',
|
||||
class: 'wechat-link-finder-author-avatar-img'
|
||||
})
|
||||
]),
|
||||
h('div', { class: 'wechat-link-finder-author-name' }, fromText || '视频号')
|
||||
])
|
||||
])
|
||||
])
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
if (isMiniProgram) {
|
||||
return h(
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<!-- 联系人信息 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="session-list-item-name text-sm font-medium truncate" :class="{ 'privacy-blur': privacyMode }">{{ contact.name }}</h3>
|
||||
<h3 class="session-list-item-name text-sm truncate" :class="{ 'privacy-blur': privacyMode }">{{ contact.name }}</h3>
|
||||
<div class="flex items-center flex-shrink-0 ml-2">
|
||||
<span class="session-list-item-time text-xs">{{ contact.lastMessageTime }}</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1774499781741" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7897" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||
<path d="M118.81813333 106.3936c87.27893333-26.2144 192.03413333 75.1616 358.46826667 346.9312 18.70506667 30.5152 34.7136 55.46666667 35.60106667 55.5008 0.88746667 0 17.74933333-26.4192 37.41013333-58.70933333 165.51253333-271.53066667 270.336-371.3024 359.35573333-342.08426667 56.55893333 18.56853333 80.41813333 73.18186667 80.21333334 183.63733333-0.4096 214.86933333-103.69706667 551.59466667-188.0064 612.89813334-69.4272 50.44906667-173.1584-13.07306667-269.85813334-165.30773334-9.59146667-15.1552-18.36373333-27.57973333-19.42186666-27.61386666-1.05813333 0-9.59146667 12.32213333-18.944 27.4432-50.96106667 82.39786667-113.4592 146.26133333-167.04853334 170.7008-26.04373333 11.8784-71.33866667 13.5168-90.45333333 3.24266666-52.08746667-27.98933333-110.72853333-149.504-156.16-323.72053333C7.3728 310.95466667 21.504 135.68 118.81813333 106.3936zM848.31573333 217.088c-55.26186667 42.93973333-126.49813333 138.58133333-230.8096 309.93066667l-49.2544 80.82773333 16.86186667 30.17386667c42.35946667 75.94666667 91.30666667 139.81013333 130.79893333 170.66666666 26.76053333 20.95786667 35.60106667 16.55466667 58.9824-29.4912 73.5232-144.55466667 136.192-440.7296 115.712-547.19146666-6.144-32.0512-15.80373333-35.46453333-42.2912-14.91626667zM143.73546667 207.9744c-19.72906667 19.49013333-14.60906667 145.8176 10.99093333 271.90613333 30.89066667 152.23466667 95.91466667 329.3184 124.5184 339.2512 27.81866667 9.65973333 104.31146667-77.824 164.38613333-188.0064l13.14133334-24.13226666-42.15466667-69.18826667c-112.98133333-185.344-186.64106667-284.3648-240.8448-323.72053333-16.65706667-12.0832-22.7328-13.312-30.03733333-6.10986667z" fill="#FF9908" p-id="7898"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1220,6 +1220,70 @@ def _parse_app_message(text: str) -> dict[str, Any]:
|
||||
"linkStyle": link_style,
|
||||
}
|
||||
|
||||
if app_type == 51:
|
||||
# 视频号分享(Finder / Channels)
|
||||
# 常见特征:
|
||||
# - title 是「当前版本不支持展示该内容,请升级至最新版本。」
|
||||
# - 真正标题在 <finderFeed><desc> 或其它 finder 节点里
|
||||
finder_feed = _extract_xml_tag_text(text, "finderFeed")
|
||||
finder_desc = (
|
||||
(_extract_xml_tag_text(finder_feed, "desc") if finder_feed else "")
|
||||
or _extract_xml_tag_text(text, "finderdesc")
|
||||
or des
|
||||
)
|
||||
finder_nickname = (
|
||||
_extract_xml_tag_text(text, "findernickname")
|
||||
or _extract_xml_tag_text(text, "finder_nickname")
|
||||
or (_extract_xml_tag_text(finder_feed, "nickname") if finder_feed else "")
|
||||
or (_extract_xml_tag_text(finder_feed, "findernickname") if finder_feed else "")
|
||||
)
|
||||
finder_username = (
|
||||
_extract_xml_tag_text(text, "finderusername")
|
||||
or _extract_xml_tag_text(text, "finder_username")
|
||||
or (_extract_xml_tag_text(finder_feed, "username") if finder_feed else "")
|
||||
or (_extract_xml_tag_text(finder_feed, "finderusername") if finder_feed else "")
|
||||
)
|
||||
|
||||
thumb_url = _normalize_xml_url(
|
||||
_extract_xml_tag_or_attr(text, "thumburl")
|
||||
or _extract_xml_tag_or_attr(text, "cdnthumburl")
|
||||
or _extract_xml_tag_or_attr(text, "coverurl")
|
||||
or _extract_xml_tag_or_attr(text, "cover")
|
||||
or (_extract_xml_tag_or_attr(finder_feed, "thumbUrl") if finder_feed else "")
|
||||
or (_extract_xml_tag_or_attr(finder_feed, "thumburl") if finder_feed else "")
|
||||
or (_extract_xml_tag_or_attr(finder_feed, "coverUrl") if finder_feed else "")
|
||||
or (_extract_xml_tag_or_attr(finder_feed, "coverurl") if finder_feed else "")
|
||||
)
|
||||
|
||||
finder_url = url or _normalize_xml_url(
|
||||
(_extract_xml_tag_text(finder_feed, "url") if finder_feed else "")
|
||||
or (_extract_xml_tag_text(text, "playurl"))
|
||||
or (_extract_xml_tag_text(text, "dataurl"))
|
||||
)
|
||||
|
||||
display_title = str(title or "").strip()
|
||||
if (not display_title) or ("不支持" in display_title):
|
||||
display_title = str(finder_desc or "").strip()
|
||||
if not display_title:
|
||||
display_title = str(des or "").strip()
|
||||
display_title = display_title or "[视频号]"
|
||||
|
||||
summary_text = str(finder_desc or "").strip() or display_title
|
||||
from_display = str(finder_nickname or source_display_name or "").strip() or "视频号"
|
||||
from_u = str(finder_username or source_username or "").strip()
|
||||
|
||||
return {
|
||||
"renderType": "link",
|
||||
"content": summary_text,
|
||||
"title": display_title,
|
||||
"url": finder_url or "",
|
||||
"thumbUrl": thumb_url or "",
|
||||
"from": from_display,
|
||||
"fromUsername": from_u,
|
||||
"linkType": "finder",
|
||||
"linkStyle": "finder",
|
||||
}
|
||||
|
||||
if app_type in (33, 36):
|
||||
# 小程序分享(WeChat v4 常见:local_type = 49 + (33<<32) / 49 + (36<<32))
|
||||
# 注:部分 payload 的 <url> 为空;前端会按需渲染为不可点击卡片。
|
||||
|
||||
@@ -1626,12 +1626,13 @@ def sync_chat_realtime_messages(
|
||||
|
||||
inserted = 0
|
||||
backfilled = 0
|
||||
if new_rows and (not name2id_synced):
|
||||
_best_effort_upsert_output_name2id_rows(
|
||||
msg_conn,
|
||||
account_name=account_dir.name,
|
||||
rows=new_rows,
|
||||
)
|
||||
if new_rows:
|
||||
if not name2id_synced:
|
||||
_best_effort_upsert_output_name2id_rows(
|
||||
msg_conn,
|
||||
account_name=account_dir.name,
|
||||
rows=new_rows,
|
||||
)
|
||||
|
||||
# Insert older -> newer to keep sqlite btree locality similar to existing data.
|
||||
values = [tuple(r.get(c) for c in insert_cols) for r in reversed(new_rows)]
|
||||
@@ -1991,12 +1992,13 @@ def _sync_chat_realtime_messages_for_table(
|
||||
|
||||
inserted = 0
|
||||
backfilled = 0
|
||||
if new_rows and (not name2id_synced):
|
||||
_best_effort_upsert_output_name2id_rows(
|
||||
msg_conn,
|
||||
account_name=account_dir.name,
|
||||
rows=new_rows,
|
||||
)
|
||||
if new_rows:
|
||||
if not name2id_synced:
|
||||
_best_effort_upsert_output_name2id_rows(
|
||||
msg_conn,
|
||||
account_name=account_dir.name,
|
||||
rows=new_rows,
|
||||
)
|
||||
|
||||
values = [tuple(r.get(c) for c in insert_cols) for r in reversed(new_rows)]
|
||||
insert_t0 = time.perf_counter()
|
||||
|
||||
@@ -112,6 +112,125 @@ class TestChatRealtimeName2IdSync(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_sync_still_inserts_new_messages_when_name2id_is_up_to_date(self):
|
||||
with TemporaryDirectory() as td:
|
||||
account_dir = Path(td) / "acc"
|
||||
account_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
username = "wxid_friend"
|
||||
table_name = f"Msg_{hashlib.md5(username.encode('utf-8')).hexdigest()}"
|
||||
msg_db_path = account_dir / "message_0.db"
|
||||
|
||||
conn = sqlite3.connect(str(msg_db_path))
|
||||
try:
|
||||
conn.execute("CREATE TABLE Name2Id (user_name TEXT, is_session INTEGER DEFAULT 1)")
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE "{table_name}" (
|
||||
local_id INTEGER PRIMARY KEY,
|
||||
server_id INTEGER,
|
||||
local_type INTEGER,
|
||||
sort_seq INTEGER,
|
||||
real_sender_id INTEGER,
|
||||
create_time INTEGER,
|
||||
message_content TEXT,
|
||||
compress_content BLOB,
|
||||
packed_info_data BLOB
|
||||
)
|
||||
""".format(table_name=table_name)
|
||||
)
|
||||
conn.execute("INSERT INTO Name2Id(rowid, user_name, is_session) VALUES (1, ?, 1)", ("acc",))
|
||||
conn.execute("INSERT INTO Name2Id(rowid, user_name, is_session) VALUES (2, ?, 1)", (username,))
|
||||
conn.execute(
|
||||
f'INSERT INTO "{table_name}" '
|
||||
"(local_id, server_id, local_type, sort_seq, real_sender_id, create_time, message_content, compress_content, packed_info_data) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(10, 10010, 1, 10, 2, 1710000010, "old", None, None),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
session_conn = sqlite3.connect(str(account_dir / "session.db"))
|
||||
try:
|
||||
session_conn.execute(
|
||||
"""
|
||||
CREATE TABLE SessionTable (
|
||||
username TEXT PRIMARY KEY,
|
||||
summary TEXT DEFAULT '',
|
||||
last_timestamp INTEGER DEFAULT 0,
|
||||
sort_timestamp INTEGER DEFAULT 0,
|
||||
last_msg_locald_id INTEGER DEFAULT 0,
|
||||
last_msg_type INTEGER DEFAULT 0,
|
||||
last_msg_sub_type INTEGER DEFAULT 0,
|
||||
last_msg_sender TEXT DEFAULT ''
|
||||
)
|
||||
"""
|
||||
)
|
||||
session_conn.commit()
|
||||
finally:
|
||||
session_conn.close()
|
||||
|
||||
def _fake_exec_query(_handle, *, kind, path, sql):
|
||||
self.assertEqual(kind, "message")
|
||||
self.assertTrue(str(path).endswith("message_0.db"))
|
||||
if "COUNT(1)" in sql:
|
||||
return [{"c": 2, "mx": 2}]
|
||||
raise AssertionError(f"Unexpected SQL: {sql}")
|
||||
|
||||
live_messages = [
|
||||
{
|
||||
"local_id": 11,
|
||||
"server_id": 10011,
|
||||
"local_type": 1,
|
||||
"sort_seq": 11,
|
||||
"real_sender_id": 2,
|
||||
"create_time": 1710000011,
|
||||
"message_content": "new message",
|
||||
"compress_content": None,
|
||||
"sender_username": username,
|
||||
}
|
||||
]
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
chat_router,
|
||||
"_resolve_db_storage_message_paths",
|
||||
return_value=(Path(td) / "live_message_0.db", Path(td) / "message_resource.db"),
|
||||
),
|
||||
patch.object(chat_router, "_wcdb_exec_query", side_effect=_fake_exec_query),
|
||||
patch.object(chat_router, "_wcdb_get_messages", side_effect=[list(live_messages)]),
|
||||
patch.object(chat_router, "_best_effort_upsert_output_name2id_rows") as mock_upsert_name2id,
|
||||
):
|
||||
result = chat_router._sync_chat_realtime_messages_for_table(
|
||||
account_dir=account_dir,
|
||||
rt_conn=_DummyConn(),
|
||||
username=username,
|
||||
msg_db_path=msg_db_path,
|
||||
table_name=table_name,
|
||||
max_scan=50,
|
||||
backfill_limit=0,
|
||||
)
|
||||
|
||||
self.assertEqual(result.get("inserted"), 1)
|
||||
mock_upsert_name2id.assert_not_called()
|
||||
|
||||
conn = sqlite3.connect(str(msg_db_path))
|
||||
try:
|
||||
rows = conn.execute(
|
||||
f'SELECT local_id, server_id, real_sender_id, create_time, message_content FROM "{table_name}" ORDER BY local_id ASC'
|
||||
).fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
self.assertEqual(
|
||||
rows,
|
||||
[
|
||||
(10, 10010, 2, 1710000010, "old"),
|
||||
(11, 10011, 2, 1710000011, "new message"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -118,6 +118,36 @@ class TestParseAppMessage(unittest.TestCase):
|
||||
self.assertEqual(parsed.get("linkType"), "official_article")
|
||||
self.assertEqual(parsed.get("linkStyle"), "cover")
|
||||
|
||||
def test_finder_type_51_uses_nested_desc_and_cover(self):
|
||||
raw_text = (
|
||||
'<msg><appmsg appid="" sdkver="0">'
|
||||
'<title>当前版本不支持展示该内容,请升级至最新版本。</title>'
|
||||
'<des></des>'
|
||||
'<type>51</type>'
|
||||
'<url></url>'
|
||||
'<finderFeed>'
|
||||
'<nickname><![CDATA[央视新闻]]></nickname>'
|
||||
'<username><![CDATA[finder_cctv_news]]></username>'
|
||||
'<desc><![CDATA[微信视频号全金融行业今公布发布]]></desc>'
|
||||
'<mediaList><media>'
|
||||
'<coverUrl><![CDATA[https://finder.video.qq.com/cover.jpg]]></coverUrl>'
|
||||
'<url><![CDATA[https://channels.weixin.qq.com/web/pages/feed?feedid=abc]]></url>'
|
||||
'</media></mediaList>'
|
||||
'</finderFeed>'
|
||||
'</appmsg></msg>'
|
||||
)
|
||||
|
||||
parsed = _parse_app_message(raw_text)
|
||||
|
||||
self.assertEqual(parsed.get("renderType"), "link")
|
||||
self.assertEqual(parsed.get("linkType"), "finder")
|
||||
self.assertEqual(parsed.get("title"), "微信视频号全金融行业今公布发布")
|
||||
self.assertEqual(parsed.get("content"), "微信视频号全金融行业今公布发布")
|
||||
self.assertEqual(parsed.get("from"), "央视新闻")
|
||||
self.assertEqual(parsed.get("fromUsername"), "finder_cctv_news")
|
||||
self.assertEqual(parsed.get("thumbUrl"), "https://finder.video.qq.com/cover.jpg")
|
||||
self.assertEqual(parsed.get("url"), "https://channels.weixin.qq.com/web/pages/feed?feedid=abc")
|
||||
|
||||
def test_quote_type_5_nested_xml_refermsg_uses_inner_title(self):
|
||||
raw_text = (
|
||||
'<msg><appmsg appid="" sdkver="0">'
|
||||
|
||||
Reference in New Issue
Block a user