"""WeChat DB 读取层 — 通过 sqlcipher CLI 解密查询(只读)""" import subprocess import json import config def query(sql: str) -> list[dict]: """执行只读 SQL 查询,返回 dict 列表""" acct = config.get_current_account() cmd = [ "su", "-c", " ".join([ config.SQLCIPHER_BIN, acct["db_path"], f'-cmd "PRAGMA key=' + acct["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 count_sessions() -> int: """统计不同 talker 数量""" sql = "SELECT COUNT(DISTINCT talker) AS cnt FROM message" rows = query(sql) return rows[0]["cnt"] if rows else 0 def get_session_list(page: int = 1, per_page: int = 10, sort: str = 'time_desc') -> list[dict]: """获取会话列表:每个 talker 的最新消息 + 未读数。支持分页+排序。""" # 边界保护 page = max(1, page) per_page = max(1, min(500, per_page)) offset = (page - 1) * per_page # sort clause if sort == 'time_asc': sort_clause = "m.createTime ASC" elif sort == 'name': # name sort needs subquery because display_name is computed sql = f""" SELECT * FROM ( 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 display_name ASC LIMIT {per_page} OFFSET {offset} """ return query(sql) else: # time_desc (default) sort_clause = "m.createTime DESC" 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 {sort_clause} LIMIT {per_page} OFFSET {offset} """ 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} """ rows = query(sql) # Extract sender_username from content for group messages import re is_group = talker.endswith("@chatroom") for row in rows: if is_group and row.get("isSend") != 1: content = row.get("content") or "" m = re.match(r"^([^:\n<]+):\n", content) row["sender_username"] = m.group(1) if m else None elif not is_group: row["sender_username"] = talker else: row["sender_username"] = None return rows 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 def get_talker_infos_batch(talkers: list[str]) -> dict[str, str]: """批量获取 talker 名称,最多 2 次 DB 查询(chatroom + contact)""" if not talkers: return {} result = {} chatrooms = [t for t in talkers if t.endswith("@chatroom")] others = [t for t in talkers if not t.endswith("@chatroom")] if chatrooms: placeholders = ",".join(f"'{t}'" for t in chatrooms) sql = f""" SELECT c.chatroomname AS talker, COALESCE(NULLIF(r.nickname, ''), c.displayname, c.chatroomname) AS name FROM chatroom c LEFT JOIN rcontact r ON c.chatroomname = r.username WHERE c.chatroomname IN ({placeholders}) """ for row in query(sql): result[row.get("talker", "")] = row.get("name", "") if others: placeholders = ",".join(f"'{t}'" for t in others) sql = f""" SELECT username AS talker, COALESCE(NULLIF(conRemark, ''), NULLIF(nickname, ''), username) AS name FROM rcontact WHERE username IN ({placeholders}) """ for row in query(sql): result[row.get("talker", "")] = row.get("name", "") return result