wechat_db.py 4.65 KB
"""WeChat DB 读取层 — 通过 sqlcipher CLI 解密查询(只读)"""

import subprocess
import json
import config


def query(sql: str) -> list[dict]:
    """执行只读 SQL 查询,返回 dict 列表"""
    cmd = [
        "su", "-c",
        " ".join([
            config.SQLCIPHER_BIN,
            config.WECHAT_DB,
            f'-cmd "PRAGMA key={config.WECHAT_KEY};"',
            '-cmd "PRAGMA cipher_compatibility=1;"',
            '-cmd "PRAGMA cipher_page_size=1024;"',
            '-cmd "PRAGMA query_only=true;"',
            '-cmd ".mode json"',
            f'"{sql}"',
        ])
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
    if result.returncode != 0:
        stderr = result.stderr.strip()
        if stderr:
            raise RuntimeError(f"SQLCipher error: {stderr[:500]}")
        return []
    output = result.stdout.strip()
    if not output:
        return []
    # sqlcipher sometimes prints "ok\n" before the JSON
    if output.startswith("ok"):
        output = output[2:].strip()
    try:
        return json.loads(output)
    except json.JSONDecodeError:
        # 可能返回多行 JSON,取第一行
        lines = output.strip().split("\n")
        rows = []
        for line in lines:
            line = line.strip()
            if line:
                try:
                    rows.append(json.loads(line))
                except json.JSONDecodeError:
                    pass
        return rows


def get_session_list(limit: int = 200) -> list[dict]:
    """获取会话列表:每个 talker 的最新消息 + 未读数"""
    sql = f"""
    SELECT 
        m.talker,
        CASE
            WHEN m.talker LIKE '%@chatroom' THEN
                COALESCE(NULLIF(r.nickname, ''), c.displayname, m.talker)
            ELSE
                COALESCE(NULLIF(r.conRemark, ''), r.nickname, m.talker)
        END AS display_name,
        COALESCE(r.conRemark, '') AS remark,
        m.content,
        m.createTime,
        m.type,
        m.isSend,
        m.imgPath,
        CASE
            WHEN m.talker LIKE '%@chatroom' THEN 'group'
            WHEN m.talker LIKE 'gh_%' THEN 'official'
            ELSE 'personal'
        END AS talker_type
    FROM message m
    LEFT JOIN rcontact r ON m.talker = r.username
    LEFT JOIN chatroom c ON m.talker = c.chatroomname
    WHERE m.createTime = (
        SELECT MAX(m2.createTime) FROM message m2 WHERE m2.talker = m.talker
    )
    ORDER BY m.createTime DESC
    LIMIT {limit}
    """
    return query(sql)


def get_messages(talker: str, limit: int = 100, offset: int = 0) -> list[dict]:
    """获取指定 talker 的消息列表"""
    sql = f"""
    SELECT 
        m.msgId,
        m.msgSvrId,
        m.type,
        m.isSend,
        m.talker,
        m.content,
        m.createTime,
        m.imgPath
    FROM message m
    WHERE m.talker = '{talker}'
    ORDER BY m.createTime DESC
    LIMIT {limit} OFFSET {offset}
    """
    return query(sql)


def search_talker(query_text: str, limit: int = 50) -> list[dict]:
    """搜索会话"""
    sql = f"""
    SELECT 
        r.username AS talker,
        COALESCE(r.conRemark, r.nickname, r.username) AS display_name,
        r.nickname,
        r.conRemark,
        CASE
            WHEN r.username LIKE '%@chatroom' THEN 'group'
            WHEN r.username LIKE 'gh_%' THEN 'official'
            ELSE 'personal'
        END AS talker_type
    FROM rcontact r
    WHERE r.nickname LIKE '%{query_text}%'
       OR r.conRemark LIKE '%{query_text}%'
       OR r.username LIKE '%{query_text}%'
    LIMIT {limit}
    """
    # Also search chatroom
    sql2 = f"""
    SELECT 
        c.chatroomname AS talker,
        c.displayname AS display_name,
        '' AS nickname,
        '' AS conRemark,
        'group' AS talker_type
    FROM chatroom c
    WHERE c.displayname LIKE '%{query_text}%'
    LIMIT {limit}
    """
    rows = query(sql) + query(sql2)
    return rows[:limit]


def get_talker_info(talker: str) -> dict | None:
    """获取单个 talker 的详细信息"""
    if talker.endswith("@chatroom"):
        sql = f"""
        SELECT 
            c.chatroomname AS talker,
            COALESCE(NULLIF(r.nickname, ''), c.displayname, c.chatroomname) AS displayname,
            c.memberCount
        FROM chatroom c
        LEFT JOIN rcontact r ON c.chatroomname = r.username
        WHERE c.chatroomname = '{talker}'
        """
    else:
        sql = f"""
        SELECT username AS talker, nickname, alias, conRemark,
               CASE WHEN username LIKE 'gh_%' THEN 'official'
                    ELSE 'personal' END AS talker_type
        FROM rcontact WHERE username = '{talker}'
        """
    rows = query(sql)
    return rows[0] if rows else None