WXUserReader.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. using K4os.Compression.LZ4.Encoders;
  2. using K4os.Compression.LZ4;
  3. using SQLite;
  4. using System;
  5. using System.Collections;
  6. using System.Collections.Generic;
  7. using System.Collections.ObjectModel;
  8. using System.IO;
  9. using System.Linq;
  10. using System.Net.Mime;
  11. using System.Security.Cryptography;
  12. using System.Text;
  13. using System.Threading.Tasks;
  14. using System.Windows.Interop;
  15. using System.Windows.Media.Imaging;
  16. using System.Xml;
  17. using System.Xml.Linq;
  18. using WechatBakTool.Helpers;
  19. using WechatBakTool.Model;
  20. using System.Windows;
  21. using System.Net.Http;
  22. using System.Reflection.Metadata;
  23. using System.Threading;
  24. namespace WechatBakTool
  25. {
  26. public class WXUserReader
  27. {
  28. private Dictionary<string, SQLiteConnection> DBInfo = new Dictionary<string, SQLiteConnection>();
  29. private UserBakConfig? UserBakConfig = null;
  30. private Hashtable HeadImgCache = new Hashtable();
  31. private Hashtable UserNameCache = new Hashtable();
  32. private Hashtable EmojiCache = new Hashtable();
  33. private HttpClient httpClient = new HttpClient();
  34. public WXUserReader(UserBakConfig userBakConfig) {
  35. string path = Path.Combine(userBakConfig.UserWorkspacePath, "DecDB");
  36. UserBakConfig = userBakConfig;
  37. LoadDB(path);
  38. InitCache();
  39. }
  40. public void LoadDB(string path)
  41. {
  42. string[] dbFileList = Directory.GetFiles(path);
  43. foreach (var item in dbFileList)
  44. {
  45. FileInfo fileInfo = new FileInfo(item);
  46. if (fileInfo.Extension != ".db")
  47. continue;
  48. SQLiteConnection con = new SQLiteConnection(item);
  49. string dbName = fileInfo.Name.Split('.')[0];
  50. DBInfo.Add(dbName, con);
  51. }
  52. }
  53. private SQLiteConnection? getCon(string name)
  54. {
  55. if (DBInfo.ContainsKey(name))
  56. {
  57. return DBInfo[name];
  58. }
  59. else
  60. {
  61. return null;
  62. }
  63. }
  64. public void InitCache()
  65. {
  66. SQLiteConnection? con = getCon("Misc");
  67. if (con == null)
  68. return;
  69. string query = @"SELECT * FROM ContactHeadImg1";
  70. List<ContactHeadImg> imgs = con.Query<ContactHeadImg>(query);
  71. foreach(ContactHeadImg item in imgs)
  72. {
  73. if (!HeadImgCache.ContainsKey(item.usrName))
  74. {
  75. HeadImgCache.Add(item.usrName, item);
  76. }
  77. }
  78. List<WXContact> contacts = GetWXContacts(null, true).ToList();
  79. foreach(WXContact contact in contacts)
  80. {
  81. if (!UserNameCache.ContainsKey(contact.UserName))
  82. UserNameCache.Add(contact.UserName, contact);
  83. }
  84. }
  85. public void EmojiCacheInit()
  86. {
  87. string emoji_path = Path.Combine(UserBakConfig!.UserWorkspacePath, "Emoji");
  88. if (Directory.Exists(emoji_path))
  89. {
  90. string[] files = Directory.GetFiles(emoji_path);
  91. foreach (string file in files)
  92. {
  93. FileInfo fileInfo = new FileInfo(file);
  94. string[] names = fileInfo.Name.Split(".");
  95. if (!EmojiCache.ContainsKey(names[0]))
  96. {
  97. EmojiCache.Add(names[0], 1);
  98. }
  99. }
  100. }
  101. }
  102. public void PreDownloadEmoji()
  103. {
  104. if (UserBakConfig == null)
  105. return;
  106. HttpClientHandler handler = new HttpClientHandler() { AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate };
  107. HttpClient httpClient = new HttpClient(handler);
  108. List<WXMsg> msgs = GetTypeMsg("47");
  109. int i = 0;
  110. EmojiCacheInit();
  111. foreach (var msg in msgs)
  112. {
  113. i++;
  114. if (i % 5 == 0)
  115. {
  116. // 每5次让下载线程休息1秒
  117. Thread.Sleep(1000);
  118. }
  119. try
  120. {
  121. XmlDocument xmlDocument = new XmlDocument();
  122. xmlDocument.LoadXml(msg.StrContent);
  123. XmlNode? node = xmlDocument.SelectSingleNode("/msg/emoji");
  124. if (node != null)
  125. {
  126. if (node.Attributes != null)
  127. {
  128. string type = "";
  129. string md5 = "";
  130. string url = "";
  131. XmlNode? item = node.Attributes.GetNamedItem("type");
  132. type = item != null ? item.InnerText : "";
  133. item = node.Attributes.GetNamedItem("md5");
  134. md5 = item != null ? item.InnerText : "";
  135. item = node.Attributes.GetNamedItem("cdnurl");
  136. url = item != null ? item.InnerText : "";
  137. if (EmojiCache.ContainsKey(md5))
  138. continue;
  139. if (url == "")
  140. continue;
  141. else
  142. {
  143. string path = Path.Combine(UserBakConfig.UserWorkspacePath, "Emoji", md5 + ".jpg");
  144. try
  145. {
  146. HttpResponseMessage res = httpClient.GetAsync(url).Result;
  147. if (res.IsSuccessStatusCode)
  148. {
  149. using (FileStream fs = File.Create(path))
  150. {
  151. res.Content.ReadAsStream().CopyTo(fs);
  152. }
  153. }
  154. }
  155. catch (Exception ex)
  156. {
  157. }
  158. }
  159. }
  160. }
  161. }
  162. catch (Exception ex)
  163. {
  164. }
  165. }
  166. }
  167. public byte[]? GetHeadImgCahce(string username)
  168. {
  169. if (HeadImgCache.ContainsKey(username))
  170. {
  171. ContactHeadImg? img = HeadImgCache[username] as ContactHeadImg;
  172. if (img == null)
  173. return null;
  174. else
  175. return img.smallHeadBuf;
  176. }
  177. return null;
  178. }
  179. public int[] GetWXCount()
  180. {
  181. SQLiteConnection? con = getCon("MicroMsg");
  182. if (con == null)
  183. return new int[] { 0, 0 };
  184. string query = @"select count(*) as count from contact where type != 4";
  185. int userCount = con.Query<WXCount>(query)[0].Count;
  186. int msgCount = 0;
  187. for (int i = 0; i <= 99; i++)
  188. {
  189. con = getCon("MSG" + i.ToString());
  190. if (con == null)
  191. return new int[] { userCount, msgCount };
  192. query = "select count(*) as count from MSG";
  193. msgCount += con.Query<WXCount>(query)[0].Count;
  194. }
  195. return new int[] { userCount, msgCount };
  196. }
  197. public ObservableCollection<WXContact> GetWXContacts(string? name = null,bool all = false)
  198. {
  199. SQLiteConnection? con = getCon("MicroMsg");
  200. if (con == null)
  201. return new ObservableCollection<WXContact>();
  202. string query = @"select contact.*,session.strContent,contactHeadImgUrl.smallHeadImgUrl,contactHeadImgUrl.bigHeadImgUrl from contact
  203. left join session on session.strUsrName = contact.username
  204. left join contactHeadImgUrl on contactHeadImgUrl.usrName = contact.username
  205. where type != 4 {searchName}
  206. order by nOrder desc";
  207. if (all)
  208. {
  209. query = query.Replace("where type != 4 ", "");
  210. }
  211. List<WXContact>? contacts = null;
  212. if (name != null)
  213. {
  214. query = query.Replace("{searchName}", " and (username like ? or alias like ? or nickname like ? or remark like ?)");
  215. contacts = con.Query<WXContact>(query, $"%{name}%", $"%{name}%", $"%{name}%", $"%{name}%");
  216. }
  217. else
  218. {
  219. query = query.Replace("{searchName}", "");
  220. contacts = con.Query<WXContact>(query);
  221. }
  222. foreach (WXContact contact in contacts)
  223. {
  224. if(contact.Remark != "")
  225. contact.NickName = contact.Remark;
  226. byte[]? imgBytes = GetHeadImgCahce(contact.UserName);
  227. if (imgBytes != null)
  228. {
  229. try
  230. {
  231. MemoryStream stream = new MemoryStream(imgBytes);
  232. BitmapImage bitmapImage = new BitmapImage();
  233. bitmapImage.BeginInit();
  234. bitmapImage.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
  235. bitmapImage.StreamSource = stream;
  236. bitmapImage.EndInit();
  237. bitmapImage.Freeze();
  238. contact.Avatar = bitmapImage;
  239. }
  240. catch
  241. {
  242. #if DEBUG
  243. File.AppendAllText("debug.log", string.Format("[D]{0} {1}:{2}\n", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "BitmapConvert Err=>Length", imgBytes.Length));
  244. #endif
  245. }
  246. }
  247. else
  248. continue;
  249. }
  250. return new ObservableCollection<WXContact>(contacts);
  251. }
  252. public List<WXUserImg>? GetUserImgs()
  253. {
  254. SQLiteConnection? con = getCon("MicroMsg");
  255. if (con == null)
  256. return null;
  257. string query = "select * from contactHeadImgUrl";
  258. return con.Query<WXUserImg>(query);
  259. }
  260. public List<WXChatRoom>? GetWXChatRooms()
  261. {
  262. SQLiteConnection? con = getCon("MicroMsg");
  263. if (con == null)
  264. return null;
  265. string query = "select * from ChatRoom";
  266. return con.Query<WXChatRoom>(query);
  267. }
  268. public List<WXMsg> GetTypeMsg(string type)
  269. {
  270. List<WXMsg> tmp = new List<WXMsg>();
  271. for (int i = 0; i <= 99; i++)
  272. {
  273. SQLiteConnection? con = getCon("MSG" + i.ToString());
  274. if (con == null)
  275. return tmp;
  276. string query = "select * from MSG where Type=?";
  277. List<WXMsg> wXMsgs = con.Query<WXMsg>(query, type);
  278. tmp.AddRange(wXMsgs);
  279. }
  280. return tmp;
  281. }
  282. public List<WXMsg>? GetWXMsgs(string uid,string msg = "")
  283. {
  284. List<WXMsg> tmp = new List<WXMsg>();
  285. for (int i = 0; i <= 99; i++)
  286. {
  287. SQLiteConnection? con = getCon("MSG" + i.ToString());
  288. if (con == null)
  289. return tmp;
  290. List<WXMsg>? wXMsgs = null;
  291. if (msg == "")
  292. {
  293. string query = "select * from MSG where StrTalker=?";
  294. wXMsgs = con.Query<WXMsg>(query, uid);
  295. }
  296. else if (uid == "")
  297. {
  298. string query = "select * from MSG where StrContent like ?";
  299. wXMsgs = con.Query<WXMsg>(query, string.Format("%{0}%", msg));
  300. }
  301. else
  302. {
  303. string query = "select * from MSG where StrTalker=? and StrContent like ?";
  304. wXMsgs = con.Query<WXMsg>(query, uid, string.Format("%{0}%", msg));
  305. }
  306. foreach (WXMsg w in wXMsgs)
  307. {
  308. if (UserNameCache.ContainsKey(w.StrTalker))
  309. {
  310. WXContact? contact = UserNameCache[w.StrTalker] as WXContact;
  311. if (contact != null)
  312. {
  313. if (contact.Remark != "")
  314. w.NickName = contact.Remark;
  315. else
  316. w.NickName = contact.NickName;
  317. }
  318. }
  319. if (uid.Contains("@chatroom"))
  320. {
  321. string userId = "";
  322. if (w.BytesExtra == null)
  323. continue;
  324. string sl = BitConverter.ToString(w.BytesExtra).Replace("-", "");
  325. ProtoMsg protoMsg;
  326. using (MemoryStream stream = new MemoryStream(w.BytesExtra))
  327. {
  328. protoMsg = ProtoBuf.Serializer.Deserialize<ProtoMsg>(stream);
  329. }
  330. if (protoMsg.TVMsg != null)
  331. {
  332. foreach (TVType _tmp in protoMsg.TVMsg)
  333. {
  334. if (_tmp.Type == 1)
  335. userId = _tmp.TypeValue;
  336. }
  337. }
  338. if (!w.IsSender)
  339. {
  340. if (UserNameCache.ContainsKey(userId))
  341. {
  342. WXContact? contact = UserNameCache[userId] as WXContact;
  343. if (contact != null)
  344. w.NickName = contact.Remark == "" ? contact.NickName : contact.Remark;
  345. }
  346. else
  347. {
  348. w.NickName = userId;
  349. }
  350. }
  351. }
  352. if (w.IsSender)
  353. w.NickName = "我";
  354. if (w.Type != 1)
  355. {
  356. if (w.Type == 10000)
  357. {
  358. w.Type = 1;
  359. w.NickName = "系统消息";
  360. w.StrContent = w.StrContent.Replace("<revokemsg>", "").Replace("</revokemsg>", "");
  361. }
  362. else
  363. {
  364. w.StrContent = "[界面未支持格式]Type=" + w.Type;
  365. }
  366. }
  367. tmp.Add(w);
  368. }
  369. }
  370. return tmp;
  371. }
  372. public List<WXSessionAttachInfo>? GetWXMsgAtc()
  373. {
  374. SQLiteConnection? con = getCon("MultiSearchChatMsg");
  375. if (con == null)
  376. return null;
  377. string query = "select * from SessionAttachInfo";
  378. List<WXSessionAttachInfo> list = con.Query<WXSessionAttachInfo>(query);
  379. if (list.Count != 0)
  380. {
  381. return list;
  382. }
  383. else
  384. return null;
  385. }
  386. public WXSessionAttachInfo? GetWXMsgAtc(WXMsg msg)
  387. {
  388. SQLiteConnection? con = getCon("MultiSearchChatMsg");
  389. if (con == null)
  390. return null;
  391. string query = "select * from SessionAttachInfo where msgId = ? order by attachsize desc";
  392. List<WXSessionAttachInfo> list = con.Query<WXSessionAttachInfo>(query, msg.MsgSvrID);
  393. if (list.Count != 0)
  394. {
  395. //部分附件可能有md5校验,这里移除校验,给的是正确路径
  396. WXSessionAttachInfo acc = list[0];
  397. int index = acc.attachPath.IndexOf(".dat");
  398. int index2 = acc.attachPath.IndexOf(".dat");
  399. if (acc.attachPath.Length - index > 10 && index != -1)
  400. {
  401. acc.attachPath = acc.attachPath.Substring(0, acc.attachPath.Length - 32);
  402. }
  403. if (acc.attachPath.Length - index2 > 10 && index2 != -1)
  404. {
  405. acc.attachPath = acc.attachPath.Substring(0, acc.attachPath.Length - 32);
  406. }
  407. return acc;
  408. }
  409. else
  410. return null;
  411. }
  412. public WXMediaMsg? GetVoiceMsg(WXMsg msg)
  413. {
  414. for (int i = 0; i <= 99; i++)
  415. {
  416. SQLiteConnection? con = getCon("MediaMSG" + i.ToString());
  417. if (con == null)
  418. continue;
  419. string query = "select * from Media where Reserved0=?";
  420. List<WXMediaMsg> wXMsgs = con.Query<WXMediaMsg>(query, msg.MsgSvrID);
  421. if (wXMsgs.Count != 0)
  422. return wXMsgs[0];
  423. }
  424. return null;
  425. }
  426. public string? GetAttachment(WXMsgType type, WXMsg msg)
  427. {
  428. if (UserBakConfig == null)
  429. return null;
  430. string? tmpPath = Path.Combine(UserBakConfig.UserWorkspacePath, "Temp");
  431. if (!Directory.Exists(tmpPath))
  432. Directory.CreateDirectory(tmpPath);
  433. // 如果是图片和视频,从附件库中搜索
  434. string? path = null;
  435. if (type == WXMsgType.Image || type == WXMsgType.Video)
  436. {
  437. WXSessionAttachInfo? atcInfo = GetWXMsgAtc(msg);
  438. if (atcInfo == null)
  439. return null;
  440. path = atcInfo.attachPath;
  441. }
  442. // 如果是从语音,从媒体库查找
  443. else if (type == WXMsgType.Audio)
  444. {
  445. WXMediaMsg? voiceMsg = GetVoiceMsg(msg);
  446. if (voiceMsg == null)
  447. return null;
  448. if (voiceMsg.Buf == null)
  449. return null;
  450. // 从DB取音频文件到临时目录
  451. string tmp_file_path = Path.Combine(tmpPath, voiceMsg.Key + ".arm");
  452. using (FileStream stream = new FileStream(tmp_file_path, FileMode.OpenOrCreate))
  453. {
  454. stream.Write(voiceMsg.Buf, 0, voiceMsg.Buf.Length);
  455. }
  456. path = tmp_file_path;
  457. }
  458. if (path == null)
  459. return null;
  460. // 获取到原路径后,开始进行解密转移,只有图片和语音需要解密,解密后是直接归档目录
  461. if(type == WXMsgType.Image || type== WXMsgType.Audio)
  462. {
  463. path = DecryptAttachment(type, path);
  464. }
  465. else if (type == WXMsgType.Video)
  466. {
  467. string video_dir = Path.Combine(UserBakConfig.UserWorkspacePath, "Video");
  468. if(!Directory.Exists(video_dir))
  469. Directory.CreateDirectory(video_dir);
  470. FileInfo fileInfo = new FileInfo(path);
  471. // 目标视频路径
  472. string video_file_path = Path.Combine(video_dir, fileInfo.Name);
  473. // 视频的路径是相对路径,需要加上资源目录
  474. path = Path.Combine(UserBakConfig.UserResPath, path);
  475. // 原文件存在,目标不存在
  476. if (!File.Exists(video_file_path) && File.Exists(path))
  477. {
  478. // 复制
  479. File.Copy(path, video_file_path);
  480. path = video_file_path;
  481. }
  482. else if (File.Exists(video_file_path))
  483. {
  484. path = video_file_path;
  485. }
  486. else
  487. return null;
  488. }
  489. if (path == null)
  490. return null;
  491. // 改相对路径
  492. path = path.Replace(UserBakConfig.UserWorkspacePath + "\\", "");
  493. return path;
  494. }
  495. public string? DecryptAttachment(WXMsgType type, string path)
  496. {
  497. if (UserBakConfig == null)
  498. return null;
  499. string? file_path = null;
  500. switch (type)
  501. {
  502. case WXMsgType.Image:
  503. string img_dir = Path.Combine(UserBakConfig.UserWorkspacePath, "Image");
  504. if (!Directory.Exists(img_dir))
  505. Directory.CreateDirectory(img_dir);
  506. // 图片的路径是相对路径,需要加上资源目录
  507. path = Path.Combine(UserBakConfig.UserResPath, path);
  508. byte[] decFileByte = DecryptionHelper.DecImage(path);
  509. string decFiletype = DecryptionHelper.CheckFileType(decFileByte);
  510. file_path = DecryptionHelper.SaveDecImage(decFileByte, path, img_dir, decFiletype);
  511. break;
  512. case WXMsgType.Audio:
  513. string audio_dir = Path.Combine(UserBakConfig.UserWorkspacePath, "Audio");
  514. if (!Directory.Exists(audio_dir))
  515. Directory.CreateDirectory(audio_dir);
  516. FileInfo fileInfo = new FileInfo(path);
  517. string audio_file_dir = Path.Combine(audio_dir, fileInfo.Name + ".mp3");
  518. ToolsHelper.DecodeVoice(path, path + ".pcm", audio_file_dir);
  519. file_path = audio_file_dir;
  520. break;
  521. }
  522. return file_path;
  523. }
  524. public List<WXMsgGroup> GetWXMsgGroup()
  525. {
  526. List<WXMsgGroup> g = new List<WXMsgGroup>();
  527. for (int i = 0; i <= 99; i++)
  528. {
  529. SQLiteConnection? con = getCon("MSG" + i.ToString());
  530. if (con == null)
  531. return g;
  532. string query = "select StrTalker,Count(localId) as MsgCount from MSG GROUP BY StrTalker";
  533. List<WXMsgGroup> wXMsgs = con.Query<WXMsgGroup>(query);
  534. foreach (WXMsgGroup w in wXMsgs)
  535. {
  536. WXMsgGroup? tmp = g.Find(x => x.UserName == w.UserName);
  537. if (tmp == null)
  538. g.Add(w);
  539. else
  540. tmp.MsgCount += g.Count;
  541. }
  542. }
  543. return g;
  544. }
  545. }
  546. public enum WXMsgType
  547. {
  548. Image = 0,
  549. Video = 1,
  550. Audio = 2,
  551. File = 3,
  552. }
  553. }