mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 03:27:00 +08:00
343 lines
10 KiB
TypeScript
343 lines
10 KiB
TypeScript
import DatabaseConstructor from 'better-sqlite3-multiple-ciphers'
|
|
import fs from 'fs-extra'
|
|
import path from 'path'
|
|
import crypto from 'crypto'
|
|
import os from 'os'
|
|
|
|
type Database = import('better-sqlite3-multiple-ciphers').Database
|
|
|
|
export interface UserContact {
|
|
m_nsUsrName: string
|
|
nickname: string
|
|
}
|
|
|
|
export interface WechatMessage {
|
|
mesLocalID: string
|
|
mesDes: number
|
|
messageType: string
|
|
msgCreateTime: string
|
|
msgContent: string
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
[key: string]: any
|
|
}
|
|
|
|
export interface Contact {
|
|
m_nsUsrName: string
|
|
m_nsNickName: string
|
|
md5: string
|
|
type: 'user' | 'group'
|
|
}
|
|
|
|
export interface GroupMemberInfo {
|
|
m_nsUsrName: string
|
|
nickname: string
|
|
m_nsHeadImgUrl: string
|
|
}
|
|
|
|
export class WechatDb {
|
|
private static WECHAT_DIR = path.join(
|
|
os.homedir(),
|
|
'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/2.0b4.0.9'
|
|
)
|
|
|
|
private rawKey: string
|
|
private correctUserId: string | null = null
|
|
private chatDb: { name: string; db_number: string }[] | null = null
|
|
private groupMemberCache = new Map<string, GroupMemberInfo | null>()
|
|
|
|
constructor(rawKey: string) {
|
|
this.rawKey = rawKey
|
|
console.log(`Initializing WechatDb with key: ${rawKey}`)
|
|
|
|
if (!fs.existsSync(WechatDb.WECHAT_DIR)) {
|
|
throw new Error(`WeChat directory not found at ${WechatDb.WECHAT_DIR}`)
|
|
}
|
|
|
|
if (this.getUser()) {
|
|
console.log('User found, getting chat DB number')
|
|
this.chatDb = this.getChatDbNumber()
|
|
} else {
|
|
throw new Error('No valid user found or invalid key')
|
|
}
|
|
}
|
|
|
|
private connectDb(dbPath: string): Database | null {
|
|
const targetPath = path.resolve(dbPath)
|
|
if (!fs.existsSync(targetPath)) {
|
|
console.error(`❌ 错误:数据库文件不存在于路径:${targetPath}`)
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const db = new DatabaseConstructor(targetPath, {
|
|
// verbose: console.log
|
|
})
|
|
|
|
// 配置加密方案 (Cipher Scheme)
|
|
db.pragma("cipher='sqlcipher'")
|
|
db.pragma('cipher_page_size = 1024')
|
|
db.pragma('legacy=3')
|
|
db.pragma('kdf_iter = 64000')
|
|
db.pragma('cipher_hmac_algorithm = HMAC_SHA1')
|
|
db.pragma('cipher_kdf_algorithm = PBKDF2_HMAC_SHA1')
|
|
|
|
const processedKey = this.processRawKey(this.rawKey)
|
|
// console.log(`🔑 应用密钥 (前6位): ${processedKey.substring(0, 6)}...`)
|
|
|
|
db.pragma(`key = "x'${processedKey}'"`)
|
|
|
|
// 验证连接
|
|
db.prepare('SELECT count(*) as count FROM sqlite_master').get()
|
|
|
|
return db
|
|
} catch (error) {
|
|
console.error(`❌ 连接失败:`, error)
|
|
|
|
const errString = String(error)
|
|
if (errString.includes('file is not a database')) {
|
|
console.error(
|
|
"💡 诊断建议:\n1. 请确认是否已安装 'better-sqlite3-multiple-ciphers'。\n2. 检查密钥是否正确(WeChat密钥与设备强绑定)。\n3. 尝试将页大小改为 4096 (db.pragma('cipher_page_size = 4096'))。"
|
|
)
|
|
} else if (errString.includes('HMAC')) {
|
|
console.error(
|
|
'💡 诊断建议:密钥可能正确,但HMAC算法或页大小不匹配。尝试调整 cipher_hmac_algorithm。'
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
}
|
|
|
|
private processRawKey(key: string): string {
|
|
// 移除可能的 '0x' 前缀并修剪空格
|
|
return key.replace(/^0x/i, '').trim()
|
|
}
|
|
|
|
private getUser(): boolean {
|
|
const keyValuePath = path.join(WechatDb.WECHAT_DIR, 'KeyValue')
|
|
if (!fs.existsSync(keyValuePath)) return false
|
|
|
|
const contents = fs.readdirSync(keyValuePath)
|
|
const potentialUsers = contents.filter((name) => !name.startsWith('.'))
|
|
|
|
for (const user of potentialUsers) {
|
|
const dbPath = path.join(keyValuePath, user, 'KeyValue.db')
|
|
if (fs.existsSync(dbPath)) {
|
|
const db = this.connectDb(dbPath)
|
|
if (db) {
|
|
this.correctUserId = user
|
|
console.log(`Valid user found: ${user}`)
|
|
db.close()
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private getChatDbNumber(): { name: string; db_number: string }[] {
|
|
const chatDb: { name: string; db_number: string }[] = []
|
|
if (!this.correctUserId) return []
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Message', `msg_${i}.db`)
|
|
if (!fs.existsSync(dbPath)) continue
|
|
|
|
const db = this.connectDb(dbPath)
|
|
if (db) {
|
|
const tables = db
|
|
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Chat_%'")
|
|
.all() as { name: string }[]
|
|
for (const table of tables) {
|
|
chatDb.push({ name: table.name, db_number: `msg_${i}.db` })
|
|
}
|
|
db.close()
|
|
}
|
|
}
|
|
return chatDb
|
|
}
|
|
|
|
public getUserList(nicknameFilter?: string): UserContact[] {
|
|
if (!this.correctUserId) return []
|
|
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Contact/wccontact_new2.db')
|
|
const db = this.connectDb(dbPath)
|
|
if (!db) return []
|
|
|
|
let query = "SELECT m_nsUsrName, nickname FROM WCContact WHERE m_nsUsrName NOT LIKE 'gh_%'"
|
|
if (nicknameFilter) {
|
|
query += ` AND nickname LIKE '%${nicknameFilter}%'`
|
|
}
|
|
|
|
const results = db.prepare(query).all() as unknown as UserContact[]
|
|
db.close()
|
|
return results
|
|
}
|
|
|
|
public getAllGroupContacts(): Record<string, string> {
|
|
if (!this.correctUserId) return {}
|
|
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db')
|
|
const db = this.connectDb(dbPath)
|
|
if (!db) return {}
|
|
|
|
const results = db.prepare('SELECT m_nsUsrName, nickname FROM GroupContact').all() as {
|
|
m_nsUsrName: string
|
|
nickname: string
|
|
}[]
|
|
db.close()
|
|
|
|
const groupContacts: Record<string, string> = {}
|
|
for (const row of results) {
|
|
if (row.m_nsUsrName && row.nickname) {
|
|
const md5 = this.md5(row.m_nsUsrName)
|
|
groupContacts[md5] = row.nickname
|
|
}
|
|
}
|
|
return groupContacts
|
|
}
|
|
|
|
public getAllGroupMembers(): Record<string, string> {
|
|
if (!this.correctUserId) return {}
|
|
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db')
|
|
const db = this.connectDb(dbPath)
|
|
if (!db) return {}
|
|
|
|
// 查询 GroupMember 表
|
|
const results = db.prepare('SELECT m_nsUsrName, nickname FROM GroupMember').all() as {
|
|
m_nsUsrName: string
|
|
nickname: string
|
|
}[]
|
|
db.close()
|
|
|
|
const groupMembers: Record<string, string> = {}
|
|
for (const row of results) {
|
|
if (row.m_nsUsrName && row.nickname) {
|
|
// 将 wxid (m_nsUsrName) 映射到昵称
|
|
groupMembers[row.m_nsUsrName] = row.nickname
|
|
}
|
|
}
|
|
return groupMembers
|
|
}
|
|
|
|
public getGroupMember(wxid: string): GroupMemberInfo | null {
|
|
if (!this.correctUserId) return null
|
|
|
|
// 检查缓存
|
|
if (this.groupMemberCache.has(wxid)) {
|
|
return this.groupMemberCache.get(wxid) || null
|
|
}
|
|
|
|
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Group/group_new.db')
|
|
const db = this.connectDb(dbPath)
|
|
if (!db) {
|
|
this.groupMemberCache.set(wxid, null)
|
|
return null
|
|
}
|
|
|
|
const result = db
|
|
.prepare(
|
|
'SELECT m_nsUsrName, nickname, m_nsHeadImgUrl FROM GroupMember WHERE m_nsUsrName = ?'
|
|
)
|
|
.get(wxid) as GroupMemberInfo
|
|
|
|
db.close()
|
|
|
|
// 写入缓存
|
|
this.groupMemberCache.set(wxid, result || null)
|
|
|
|
return result || null
|
|
}
|
|
|
|
public getAllChatTables(): { name: string; db_number: string }[] {
|
|
return this.chatDb || []
|
|
}
|
|
|
|
public getUserMessages(userMd5: string, startTime?: number, endTime?: number): WechatMessage[] {
|
|
if (!this.chatDb || !this.correctUserId) return []
|
|
|
|
const tableName = `Chat_${userMd5}`
|
|
const tableInfo = this.chatDb.find((t) => t.name === tableName)
|
|
|
|
if (!tableInfo) {
|
|
console.error(`No table found for ${tableName}`)
|
|
return []
|
|
}
|
|
|
|
const dbPath = path.join(
|
|
WechatDb.WECHAT_DIR,
|
|
this.correctUserId,
|
|
'Message',
|
|
tableInfo.db_number
|
|
)
|
|
const db = this.connectDb(dbPath)
|
|
if (!db) return []
|
|
|
|
try {
|
|
let query = `SELECT * FROM ${tableName}`
|
|
const conditions: string[] = []
|
|
|
|
// console.log(startTime, endTime, 'time range')
|
|
|
|
if (startTime) {
|
|
conditions.push(`msgCreateTime >= ${startTime}`)
|
|
}
|
|
if (endTime) {
|
|
conditions.push(`msgCreateTime <= ${endTime}`)
|
|
}
|
|
|
|
if (conditions.length > 0) {
|
|
query += ` WHERE ${conditions.join(' AND ')}`
|
|
}
|
|
|
|
// 添加排序以确保存储正确的顺序
|
|
query += ` ORDER BY msgCreateTime ASC`
|
|
|
|
const results = db.prepare(query).all() as unknown as WechatMessage[]
|
|
db.close()
|
|
return results
|
|
} catch (e) {
|
|
console.error(`Error querying messages for ${tableName}:`, e)
|
|
db.close()
|
|
return []
|
|
}
|
|
}
|
|
|
|
public searchAllMessages(keyword: string): string | null {
|
|
if (!this.correctUserId) return null
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
const dbName = `msg_${i}.db`
|
|
const dbPath = path.join(WechatDb.WECHAT_DIR, this.correctUserId, 'Message', dbName)
|
|
if (!fs.existsSync(dbPath)) continue
|
|
|
|
const db = this.connectDb(dbPath)
|
|
if (!db) continue
|
|
|
|
const tables = db
|
|
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Chat_%'")
|
|
.all() as { name: string }[]
|
|
for (const table of tables) {
|
|
try {
|
|
// 首先检查 msgContent 列是否存在以避免错误?
|
|
// 或者像 Swift 代码中那样使用 try/catch "抑制错误"
|
|
const results = db
|
|
.prepare(`SELECT 1 FROM ${table.name} WHERE msgContent LIKE '%${keyword}%' LIMIT 1`)
|
|
.get()
|
|
if (results) {
|
|
console.log(`Found keyword in ${table.name} (${dbName})`)
|
|
db.close()
|
|
return table.name
|
|
}
|
|
} catch {
|
|
// 忽略没有 msgContent 的表
|
|
}
|
|
}
|
|
db.close()
|
|
}
|
|
return null
|
|
}
|
|
|
|
public md5(str: string): string {
|
|
return crypto.createHash('md5').update(str).digest('hex')
|
|
}
|
|
}
|