HtmlExport.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. using K4os.Compression.LZ4.Encoders;
  2. using K4os.Compression.LZ4;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Threading.Tasks;
  9. using WechatBakTool.Model;
  10. using System.Xml;
  11. using Newtonsoft.Json;
  12. using WechatBakTool.ViewModel;
  13. using System.Security.Policy;
  14. using System.Windows;
  15. using System.Xml.Linq;
  16. namespace WechatBakTool.Export
  17. {
  18. public class HtmlExport : IExport
  19. {
  20. private string HtmlBody = "";
  21. private WXSession? Session = null;
  22. private string Path = "";
  23. public void InitTemplate(WXSession session)
  24. {
  25. Session = session;
  26. HtmlBody = "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>WechatBakTool</title><style>p{margin:0px;}.msg{padding-bottom:10px;}.nickname{font-size:10px;}.content{font-size:14px;}</style></head><body>";
  27. HtmlBody += string.Format("<div class=\"msg\"><p class=\"nickname\"><b>与 {0}({1}) 的聊天记录</b></p>", Session.NickName, Session.UserName);
  28. HtmlBody += string.Format("<div class=\"msg\"><p class=\"nickname\"><b>导出时间:{0}</b></p><hr/>", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
  29. File.WriteAllText(Path, HtmlBody);
  30. }
  31. public void InitTemplate(WXContact contact, string p)
  32. {
  33. Path = p;
  34. WXSession session = new WXSession();
  35. session.NickName = contact.NickName;
  36. session.UserName = contact.UserName;
  37. InitTemplate(session);
  38. }
  39. public void Save(string path = "")
  40. {
  41. }
  42. public void SetEnd()
  43. {
  44. HtmlBody += "</body></html>";
  45. File.AppendAllText(Path, HtmlBody);
  46. }
  47. public void SetMsg(WXUserReader reader, WXContact contact,WorkspaceViewModel viewModel)
  48. {
  49. if (Session == null)
  50. throw new Exception("请初始化模版:Not Use InitTemplate");
  51. List<WXMsg>? msgList = reader.GetWXMsgs(contact.UserName);
  52. if (msgList == null)
  53. throw new Exception("获取消息失败,请确认数据库读取正常");
  54. msgList.Sort((x, y) => x.CreateTime.CompareTo(y.CreateTime));
  55. bool err = false;
  56. int msgCount = 0;
  57. HtmlBody = "";
  58. StreamWriter streamWriter = new StreamWriter(Path, true);
  59. foreach (var msg in msgList)
  60. {
  61. try
  62. {
  63. HtmlBody += string.Format("<div class=\"msg\"><p class=\"nickname\">{0} <span style=\"padding-left:10px;\">{1}</span></p>", msg.IsSender ? "我" : msg.NickName, TimeStampToDateTime(msg.CreateTime).ToString("yyyy-MM-dd HH:mm:ss"));
  64. if (msg.Type == 1)
  65. HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", msg.StrContent);
  66. else if (msg.Type == 3)
  67. {
  68. string? path = reader.GetAttachment(WXMsgType.Image, msg);
  69. if (path == null)
  70. {
  71. #if DEBUG
  72. File.AppendAllText("debug.log", string.Format("[D]{0} {1}:{2}\n", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "Img Error Path=>", path));
  73. File.AppendAllText("debug.log", string.Format("[D]{0} {1}:{2}\n", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "Img Error Msg=>", JsonConvert.SerializeObject(msg)));
  74. #endif
  75. HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", "图片转换出现错误或文件不存在");
  76. continue;
  77. }
  78. HtmlBody += string.Format("<p class=\"content\"><img src=\"{0}\" style=\"max-height:1000px;max-width:1000px;\"/></p></div>", path);
  79. }
  80. else if (msg.Type == 43)
  81. {
  82. string? path = reader.GetAttachment(WXMsgType.Video, msg);
  83. if (path == null)
  84. {
  85. HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", "视频不存在");
  86. continue;
  87. }
  88. HtmlBody += string.Format("<p class=\"content\"><video controls style=\"max-height:300px;max-width:300px;\"><source src=\"{0}\" type=\"video/mp4\" /></video></p></div>", path);
  89. }
  90. else if (msg.Type == 47)
  91. {
  92. string? path = reader.GetAttachment(WXMsgType.Emoji, msg);
  93. if (path == null)
  94. {
  95. #if DEBUG
  96. File.AppendAllText("debug.log", string.Format("[D]{0} {1}:{2}\n", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "Emoji Error Path=>", path));
  97. File.AppendAllText("debug.log", string.Format("[D]{0} {1}:{2}\n", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "Emoji Error Msg=>", JsonConvert.SerializeObject(msg)));
  98. #endif
  99. HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", "表情未预下载或加密表情");
  100. continue;
  101. }
  102. HtmlBody += string.Format("<p class=\"content\"><img src=\"{0}\" style=\"max-height:300px;max-width:300px;\"/></p></div>", path);
  103. }
  104. else if (msg.Type == 49)
  105. {
  106. if (msg.SubType == 6 || msg.SubType == 40)
  107. {
  108. string? path = reader.GetAttachment(WXMsgType.File, msg);
  109. if (path == null)
  110. {
  111. HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", "文件不存在");
  112. continue;
  113. }
  114. else
  115. {
  116. HtmlBody += string.Format("<p class=\"content\">{0}</p><p><a href=\"{1}\">点击访问</a></p></div>", "文件:" + path, path);
  117. }
  118. }
  119. else if (msg.SubType == 19)
  120. {
  121. using (var decoder = LZ4Decoder.Create(true, 64))
  122. {
  123. byte[] target = new byte[10240];
  124. int res = 0;
  125. if (msg.CompressContent != null)
  126. res = LZ4Codec.Decode(msg.CompressContent, 0, msg.CompressContent.Length, target, 0, target.Length);
  127. byte[] data = target.Skip(0).Take(res).ToArray();
  128. string xml = Encoding.UTF8.GetString(data);
  129. if (!string.IsNullOrEmpty(xml))
  130. {
  131. xml = xml.Replace("\n", "");
  132. XmlDocument xmlObj = new XmlDocument();
  133. xmlObj.LoadXml(xml);
  134. if (xmlObj.DocumentElement != null)
  135. {
  136. string title = "";
  137. string record = "";
  138. string url = "";
  139. XmlNodeList? findNode = xmlObj.DocumentElement.SelectNodes("/msg/appmsg/title");
  140. if (findNode != null)
  141. {
  142. if (findNode.Count > 0)
  143. {
  144. title = findNode[0]!.InnerText;
  145. }
  146. }
  147. HtmlBody += string.Format("<p class=\"content\">{0}</p>", title);
  148. try
  149. {
  150. findNode = xmlObj.DocumentElement.SelectNodes("/msg/appmsg/recorditem");
  151. if (findNode != null)
  152. {
  153. if (findNode.Count > 0)
  154. {
  155. XmlDocument itemObj = new XmlDocument();
  156. itemObj.LoadXml(findNode[0]!.InnerText);
  157. XmlNodeList? itemNode = itemObj.DocumentElement.SelectNodes("/recordinfo/datalist/dataitem");
  158. if (itemNode.Count > 0)
  159. {
  160. foreach (XmlNode node in itemNode)
  161. {
  162. string nodeMsg;
  163. string name = node["sourcename"].InnerText;
  164. if (node.Attributes["datatype"].InnerText == "1")
  165. nodeMsg = node["datadesc1"].InnerText;
  166. else if (node.Attributes["datatype"].InnerText == "2")
  167. nodeMsg = "不支持的消息";
  168. else
  169. nodeMsg = node["datatitle"].InnerText;
  170. HtmlBody += string.Format("<p class=\"content\">{0}:{1}</p>", name, nodeMsg);
  171. }
  172. }
  173. }
  174. }
  175. }
  176. catch
  177. {
  178. HtmlBody += string.Format("<p class=\"content\">{0}</p>", "解析异常");
  179. }
  180. }
  181. }
  182. }
  183. }
  184. else if (msg.SubType == 57)
  185. {
  186. using (var decoder = LZ4Decoder.Create(true, 64))
  187. {
  188. byte[] target = new byte[10240];
  189. int res = 0;
  190. if (msg.CompressContent != null)
  191. res = LZ4Codec.Decode(msg.CompressContent, 0, msg.CompressContent.Length, target, 0, target.Length);
  192. byte[] data = target.Skip(0).Take(res).ToArray();
  193. string xml = Encoding.UTF8.GetString(data);
  194. if (!string.IsNullOrEmpty(xml))
  195. {
  196. xml = xml.Replace("\n", "");
  197. XmlDocument xmlObj = new XmlDocument();
  198. xmlObj.LoadXml(xml);
  199. if (xmlObj.DocumentElement != null)
  200. {
  201. string title = "";
  202. XmlNodeList? findNode = xmlObj.DocumentElement.SelectNodes("/msg/appmsg/title");
  203. if (findNode != null)
  204. {
  205. if (findNode.Count > 0)
  206. {
  207. title = findNode[0]!.InnerText;
  208. }
  209. }
  210. HtmlBody += string.Format("<p class=\"content\">{0}</p>", title);
  211. XmlNode? type = xmlObj.DocumentElement.SelectSingleNode("/msg/appmsg/refermsg/type");
  212. if(type != null)
  213. {
  214. XmlNode? source = xmlObj.DocumentElement.SelectSingleNode("/msg/appmsg/refermsg/displayname");
  215. XmlNode? text = xmlObj.DocumentElement.SelectSingleNode("/msg/appmsg/refermsg/content");
  216. if(type.InnerText == "1" && source != null && text != null)
  217. {
  218. HtmlBody += string.Format("<p class=\"content\">[引用]{0}:{1}</p>", source.InnerText, text.InnerText);
  219. }
  220. else if(type.InnerText != "1" && source != null && text != null)
  221. {
  222. HtmlBody += string.Format("<p class=\"content\">[引用]{0}:非文本消息类型-{1}</p>", source.InnerText, type);
  223. }
  224. else
  225. {
  226. HtmlBody += string.Format("<p class=\"content\">未知的引用消息</p>");
  227. }
  228. }
  229. }
  230. }
  231. }
  232. }
  233. else if (msg.SubType == 57)
  234. {
  235. using (var decoder = LZ4Decoder.Create(true, 64))
  236. {
  237. byte[] target = new byte[10240];
  238. int res = 0;
  239. if (msg.CompressContent != null)
  240. res = LZ4Codec.Decode(msg.CompressContent, 0, msg.CompressContent.Length, target, 0, target.Length);
  241. byte[] data = target.Skip(0).Take(res).ToArray();
  242. string xml = Encoding.UTF8.GetString(data);
  243. if (!string.IsNullOrEmpty(xml))
  244. {
  245. xml = xml.Replace("\n", "");
  246. XmlDocument xmlObj = new XmlDocument();
  247. xmlObj.LoadXml(xml);
  248. if (xmlObj.DocumentElement != null)
  249. {
  250. string title = "";
  251. XmlNodeList? findNode = xmlObj.DocumentElement.SelectNodes("/msg/appmsg/title");
  252. if (findNode != null)
  253. {
  254. if (findNode.Count > 0)
  255. {
  256. title = findNode[0]!.InnerText;
  257. }
  258. }
  259. HtmlBody += string.Format("<p class=\"content\">{0}</p>", title);
  260. XmlNode? type = xmlObj.DocumentElement.SelectSingleNode("/msg/appmsg/refermsg/type");
  261. if(type != null)
  262. {
  263. XmlNode? source = xmlObj.DocumentElement.SelectSingleNode("/msg/appmsg/refermsg/displayname");
  264. XmlNode? text = xmlObj.DocumentElement.SelectSingleNode("/msg/appmsg/refermsg/content");
  265. if(type.InnerText == "1" && source != null && text != null)
  266. {
  267. HtmlBody += string.Format("<p class=\"content\">[引用]{0}:{1}</p>", source.InnerText, text.InnerText);
  268. }
  269. else if(type.InnerText != "1" && source != null && text != null)
  270. {
  271. HtmlBody += string.Format("<p class=\"content\">[引用]{0}:非文本消息类型-{1}</p>", source.InnerText, type);
  272. }
  273. else
  274. {
  275. HtmlBody += string.Format("<p class=\"content\">未知的引用消息</p>");
  276. }
  277. }
  278. }
  279. }
  280. }
  281. }
  282. else
  283. {
  284. using (var decoder = LZ4Decoder.Create(true, 64))
  285. {
  286. byte[] target = new byte[10240];
  287. int res = 0;
  288. if (msg.CompressContent != null)
  289. res = LZ4Codec.Decode(msg.CompressContent, 0, msg.CompressContent.Length, target, 0, target.Length);
  290. byte[] data = target.Skip(0).Take(res).ToArray();
  291. string xml = Encoding.UTF8.GetString(data);
  292. if (!string.IsNullOrEmpty(xml))
  293. {
  294. xml = xml.Replace("\n", "");
  295. XmlDocument xmlObj = new XmlDocument();
  296. xmlObj.LoadXml(xml);
  297. if (xmlObj.DocumentElement != null)
  298. {
  299. string title = "";
  300. string appName = "";
  301. string url = "";
  302. XmlNodeList? findNode = xmlObj.DocumentElement.SelectNodes("/msg/appmsg/title");
  303. if (findNode != null)
  304. {
  305. if (findNode.Count > 0)
  306. {
  307. title = findNode[0]!.InnerText;
  308. }
  309. }
  310. findNode = xmlObj.DocumentElement.SelectNodes("/msg/appmsg/sourcedisplayname");
  311. if (findNode != null)
  312. {
  313. if (findNode.Count > 0)
  314. {
  315. appName = findNode[0]!.InnerText;
  316. }
  317. }
  318. findNode = xmlObj.DocumentElement.SelectNodes("/msg/appmsg/url");
  319. if (findNode != null)
  320. {
  321. if (findNode.Count > 0)
  322. {
  323. url = findNode[0]!.InnerText;
  324. }
  325. }
  326. HtmlBody += string.Format("<p class=\"content\">{0}|{1}</p><p><a href=\"{2}\">点击访问</a></p></div>", appName, title, url);
  327. }
  328. }
  329. }
  330. }
  331. }
  332. else if (msg.Type == 34)
  333. {
  334. string? path = reader.GetAttachment(WXMsgType.Audio, msg);
  335. if (path == null)
  336. {
  337. HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", "语音不存在");
  338. continue;
  339. }
  340. HtmlBody += string.Format("<p class=\"content\"><audio controls src=\"{0}\"></audio></p></div>", path);
  341. }
  342. else
  343. {
  344. HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", "暂未支持的消息");
  345. }
  346. }
  347. catch(Exception ex)
  348. {
  349. err = true;
  350. File.AppendAllText("Err.log", JsonConvert.SerializeObject(msg));
  351. File.AppendAllText("Err.log", ex.ToString());
  352. }
  353. msgCount++;
  354. if(msgCount % 50 == 0)
  355. {
  356. streamWriter.WriteLine(HtmlBody);
  357. HtmlBody = "";
  358. viewModel.ExportCount = msgCount.ToString();
  359. }
  360. }
  361. if(msgCount % 50 != 0)
  362. {
  363. streamWriter.WriteLine(HtmlBody);
  364. HtmlBody = "";
  365. viewModel.ExportCount = msgCount.ToString();
  366. if (err)
  367. {
  368. MessageBox.Show("本次导出发生了异常,部分消息被跳过,更新至最新版本后还有此问题,请将Err.log反馈给开发,谢谢。", "错误");
  369. }
  370. }
  371. streamWriter.Close();
  372. streamWriter.Dispose();
  373. }
  374. private static DateTime TimeStampToDateTime(long timeStamp, bool inMilli = false)
  375. {
  376. DateTimeOffset dateTimeOffset = inMilli ? DateTimeOffset.FromUnixTimeMilliseconds(timeStamp) : DateTimeOffset.FromUnixTimeSeconds(timeStamp);
  377. return dateTimeOffset.LocalDateTime;
  378. }
  379. }
  380. }