HtmlExport.cs 19 KB

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