2
0

HtmlExport.cs 19 KB

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