2
0
Эх сурвалжийг харах

重构工作区,理顺工作区概念。
新增微信选择功能,实现多开微信时也能方便选择创建/解密。
UI更新,更贴合现在的想法和概念,更易于理解。
添加3.9.7.25版本信息。

Suxue 1 жил өмнө
parent
commit
03074e75a3

+ 1 - 1
App.xaml

@@ -2,7 +2,7 @@
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:local="clr-namespace:WechatPCMsgBakTool"
-             StartupUri="MainWindow.xaml">
+             StartupUri="Main.xaml">
     <Application.Resources>
          
     </Application.Resources>

+ 106 - 0
Helpers/DecryptionHelper.cs

@@ -151,6 +151,112 @@ namespace WechatPCMsgBakTool.Helpers
         {
             return BitConverter.ToString(bytes, 0).Replace("-", string.Empty).ToLower().ToUpper();
         }
+        public static byte[] DecImage(string source)
+        {
+            //读取数据
+            byte[] fileBytes = File.ReadAllBytes(source);
+            //算差异转换
+            byte key = GetImgKey(fileBytes);
+            fileBytes = ConvertData(fileBytes, key);
+            return fileBytes;
+        }
+        public static string CheckFileType(byte[] data)
+        {
+            switch (data[0])
+            {
+                case 0XFF:  //byte[] jpg = new byte[] { 0xFF, 0xD8, 0xFF };
+                    {
+                        if (data[1] == 0xD8 && data[2] == 0xFF)
+                        {
+                            return ".jpg";
+                        }
+                        break;
+                    }
+                case 0x89:  //byte[] png = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
+                    {
+                        if (data[1] == 0x50 && data[2] == 0x4E && data[7] == 0x0A)
+                        {
+                            return ".png";
+                        }
+                        break;
+                    }
+                case 0x42:  //byte[] bmp = new byte[] { 0x42, 0x4D };
+                    {
+                        if (data[1] == 0X4D)
+                        {
+                            return ".bmp";
+                        }
+                        break;
+                    }
+                case 0x47:  //byte[] gif = new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39(0x37), 0x61 };
+                    {
+                        if (data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x38 && data[5] == 0x61)
+                        {
+                            return ".gif";
+                        }
+                        break;
+                    }
+                case 0x49:  // byte[] tif = new byte[] { 0x49, 0x49, 0x2A, 0x00 };
+                    {
+                        if (data[1] == 0x49 && data[2] == 0x2A && data[3] == 0x00)
+                        {
+                            return ".tif";
+                        }
+                        break;
+                    }
+                case 0x4D:  //byte[] tif = new byte[] { 0x4D, 0x4D, 0x2A, 0x00 };
+                    {
+                        if (data[1] == 0x4D && data[2] == 0x2A && data[3] == 0x00)
+                        {
+                            return ".tif";
+                        }
+                        break;
+                    }
+            }
+
+            return ".dat";
+        }
+        private static byte GetImgKey(byte[] fileRaw)
+        {
+            byte[] raw = new byte[8];
+            for (int i = 0; i < 8; i++)
+            {
+                raw[i] = fileRaw[i];
+            }
+
+            for (byte key = 0x01; key < 0xFF; key++)
+            {
+                byte[] buf = new byte[8];
+                raw.CopyTo(buf, 0);
+
+                if (CheckFileType(ConvertData(buf, key)) != ".dat")
+                {
+                    return key;
+                }
+            }
+            return 0x00;
+        }
+        private static byte[] ConvertData(byte[] data, byte key)
+        {
+            for (int i = 0; i < data.Length; i++)
+            {
+                data[i] ^= key;
+            }
+
+            return data;
+        }
+        public static string SaveDecImage(byte[] fileRaw,string source,string to_dir,string type)
+        {
+            FileInfo fileInfo = new FileInfo(source);
+            string fileName = fileInfo.Name.Substring(0, fileInfo.Name.Length - 4);
+            string saveFilePath = Path.Combine(to_dir, fileName + type);
+            using (FileStream fileStream = File.OpenWrite(saveFilePath))
+            {
+                fileStream.Write(fileRaw, 0, fileRaw.Length);
+                fileStream.Flush();
+            }
+            return saveFilePath;
+        }
     }
 
 }

+ 13 - 0
Helpers/ProcessHelper.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using System.Runtime.InteropServices;
 using System.Text;
 using System.Threading.Tasks;
+using System.Windows;
 
 namespace WechatPCMsgBakTool.Helpers
 {
@@ -15,6 +16,18 @@ namespace WechatPCMsgBakTool.Helpers
             Process[] processes = Process.GetProcessesByName(ProcessName);
             if (processes.Length == 0)
                 return null;
+            else if(processes.Length > 1) {
+                SelectWechat selectWechat = new SelectWechat();
+                MessageBox.Show("检测到有多个微信,请选择本工作区对应的微信");
+                selectWechat.ShowDialog();
+                if (selectWechat.SelectProcess == null)
+                    return null;
+
+                Process? p = processes.ToList().Find(x => x.Id.ToString() == selectWechat.SelectProcess.ProcessId);
+                if (p == null)
+                    return null;
+                return p;
+            }
             else
                 return processes[0];
         }

+ 3 - 3
Helpers/WechatDBHelper.cs

@@ -136,10 +136,10 @@ namespace WechatPCMsgBakTool.Helpers
             }
             return "请复制目录至文本框内";
         }
-        public static void DecryUserData(byte[] key)
+        public static void DecryUserData(byte[] key,string source,string to)
         {
-            string dbPath = Path.Combine(UserWorkPath, "DB");
-            string decPath = Path.Combine(UserWorkPath, "DecDB");
+            string dbPath = source;
+            string decPath = to;
             if(!Directory.Exists(decPath))
                 Directory.CreateDirectory(decPath);
 

+ 25 - 12
HtmlExport.cs

@@ -22,6 +22,14 @@ namespace WechatPCMsgBakTool
             HtmlBody += string.Format("<div class=\"msg\"><p class=\"nickname\"><b>导出时间:{0}</b></p><hr/>", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
         }
 
+        public void InitTemplate(WXContact contact)
+        {
+            WXSession session = new WXSession();
+            session.NickName = contact.NickName;
+            session.UserName = contact.UserName;
+            InitTemplate(session);
+        }
+
         public void Save(string path = "",bool append = false)
         {
             if (!append)
@@ -41,21 +49,26 @@ namespace WechatPCMsgBakTool
             HtmlBody += "</body></html>";
         }
 
-        public void SetMsg(WXReader reader, WXSession session)
+        public void SetMsg(WXUserReader reader,WXContact contact)
         {
-            List<WXMsg> msgList = reader.GetMsgs(session.UserName);
+            if (Session == null)
+                throw new Exception("请初始化模版:Not Use InitTemplate");
+
+            List<WXMsg>? msgList = reader.GetWXMsgs(contact.UserName);
+            if (msgList == null)
+                throw new Exception("获取消息失败,请确认数据库读取正常");
+
             msgList.Sort((x, y) => x.CreateTime.CompareTo(y.CreateTime));
+
             foreach (var msg in msgList)
             {
-                if (Session == null)
-                    throw new Exception("请初始化模版:Not Use InitTemplate");
                 HtmlBody += string.Format("<div class=\"msg\"><p class=\"nickname\">{0} <span style=\"padding-left:10px;\">{1}</span></p>", msg.IsSender ? "我" : Session.NickName, TimeStampToDateTime(msg.CreateTime).ToString("yyyy-MM-dd HH:mm:ss"));
 
                 if (msg.Type == 1)
                     HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", msg.StrContent);
-                else if(msg.Type == 3)
+                else if (msg.Type == 3)
                 {
-                    string? path = reader.GetImage(msg);
+                    string? path = reader.GetAttachment(WXMsgType.Image, msg);
                     if (path == null)
                     {
                         HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", "图片转换出现错误或文件不存在");
@@ -63,9 +76,9 @@ namespace WechatPCMsgBakTool
                     }
                     HtmlBody += string.Format("<p class=\"content\"><img src=\"{0}\" style=\"max-height:1000px;max-width:1000px;\"/></p></div>", path);
                 }
-                else if(msg.Type == 43)
+                else if (msg.Type == 43)
                 {
-                    string? path = reader.GetVideo(msg);
+                    string? path = reader.GetAttachment(WXMsgType.Video, msg);
                     if (path == null)
                     {
                         HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", "视频不存在");
@@ -73,12 +86,12 @@ namespace WechatPCMsgBakTool
                     }
                     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);
                 }
-                else if(msg.Type == 34)
+                else if (msg.Type == 34)
                 {
-                    string? path = reader.GetVoice(msg);
+                    string? path = reader.GetAttachment(WXMsgType.Audio, msg);
                     if (path == null)
                     {
-                        HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", "视频不存在");
+                        HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", "语音不存在");
                         continue;
                     }
                     HtmlBody += string.Format("<p class=\"content\"><audio controls src=\"{0}\"></audio></p></div>", path);
@@ -88,8 +101,8 @@ namespace WechatPCMsgBakTool
                     HtmlBody += string.Format("<p class=\"content\">{0}</p></div>", "暂未支持的消息");
                 }
             }
-        }
 
+        }
         private static DateTime TimeStampToDateTime(long timeStamp, bool inMilli = false)
         {
             DateTimeOffset dateTimeOffset = inMilli ? DateTimeOffset.FromUnixTimeMilliseconds(timeStamp) : DateTimeOffset.FromUnixTimeSeconds(timeStamp);

+ 2 - 1
Interface/ExportInterface.cs

@@ -10,7 +10,8 @@ namespace WechatPCMsgBakTool.Interface
     public interface IExport
     {
         void InitTemplate(WXSession session);
-        void SetMsg(WXReader reader, WXSession session);
+        void InitTemplate(WXContact session);
+        void SetMsg(WXUserReader reader, WXContact session);
         void SetEnd();
         void Save(string path = "", bool append = false);
 

+ 38 - 0
Main.xaml

@@ -0,0 +1,38 @@
+<Window x:Class="WechatPCMsgBakTool.Main"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:WechatPCMsgBakTool"
+        mc:Ignorable="d"
+        WindowStartupLocation="CenterScreen"
+        Title="溯雪微信备份工具" Height="450" Width="800">
+    <Grid>
+        <ListView Name="list_workspace" Margin="15,50,0,20" HorizontalAlignment="Left" Width="230" Grid.RowSpan="2" SelectionChanged="list_workspace_SelectionChanged">
+            <ListView.View>
+                <GridView>
+                    <GridViewColumn Header="原始id" Width="140" DisplayMemberBinding="{Binding UserName,Mode=TwoWay}" />
+                    <GridViewColumn Header="是否解密" Width="80" DisplayMemberBinding="{Binding Decrypt,Mode=TwoWay}" />
+                </GridView>
+            </ListView.View>
+        </ListView>
+        <Label Content="工作区:" HorizontalAlignment="Left" Margin="15,15,0,0" VerticalAlignment="Top" Height="25" Width="58"/>
+        <Button Content="新增" Width="50" HorizontalAlignment="Left" Margin="194,20,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.343,0.521" Height="19" Click="Button_Click_1"/>
+        <Label Content="用户路径:-" Name="user_path" HorizontalAlignment="Left" Margin="278,50,0,0" VerticalAlignment="Top" Height="25" Width="500"/>
+        <Button Content="解密" IsEnabled="False" Width="50" HorizontalAlignment="Left" Margin="285,20,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.343,0.521" Name="btn_decrypt" Click="btn_decrypt_Click" Height="19"/>
+        <Button Content="读取" IsEnabled="False" Width="50" HorizontalAlignment="Left" Margin="365,20,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.343,0.521" Name="btn_read" Click="btn_read_Click" Height="19" />
+
+        <ListView Name="list_sessions" Margin="278,130,0,20" HorizontalAlignment="Left" Width="290" MouseDoubleClick="list_sessions_MouseDoubleClick">
+            <ListView.View>
+                <GridView>
+                    <GridViewColumn Header="昵称" Width="120" DisplayMemberBinding="{Binding NickName}" />
+                    <GridViewColumn Header="原始id" Width="140" DisplayMemberBinding="{Binding UserName}" />
+                </GridView>
+            </ListView.View>
+        </ListView>
+        <Button Content="导出所选人员聊天记录" HorizontalAlignment="Left" Margin="609,130,0,0" VerticalAlignment="Top" Width="142" Click="Button_Click"/>
+        <Label Content="搜索:" HorizontalAlignment="Left" Margin="278,92,0,0" VerticalAlignment="Top"/>
+        <TextBox Name="find_user" HorizontalAlignment="Left" Margin="323,96,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="120" Height="20"/>
+        <Button Name="btn_search" Content="搜索" HorizontalAlignment="Left" Margin="451,96,0,0" VerticalAlignment="Top" Width="43" Click="btn_search_Click"/>
+    </Grid>
+</Window>

+ 184 - 0
Main.xaml.cs

@@ -0,0 +1,184 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Reflection.PortableExecutable;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using WechatPCMsgBakTool.Helpers;
+using WechatPCMsgBakTool.Interface;
+using WechatPCMsgBakTool.Model;
+
+namespace WechatPCMsgBakTool
+{
+    /// <summary>
+    /// Main.xaml 的交互逻辑
+    /// </summary>
+    public partial class Main : Window
+    {
+        private UserBakConfig? CurrentUserBakConfig = null;
+        private WXUserReader? UserReader = null;
+        private ObservableCollection<UserBakConfig> userBakConfigs = new ObservableCollection<UserBakConfig>();
+        public Main()
+        {
+            InitializeComponent();
+            LoadWorkspace();
+        }
+
+        private void LoadWorkspace()
+        {
+            userBakConfigs.Clear();
+            string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "workspace");
+            if (Directory.Exists(path))
+            {
+                string[] files = Directory.GetFiles(path);
+                foreach(string file in files)
+                {
+                    string type = file.Substring(file.Length - 5, 5);
+                    if(type == ".json")
+                    {
+                        string jsonString = File.ReadAllText(file);
+                        UserBakConfig? userBakConfig = JsonConvert.DeserializeObject<UserBakConfig>(jsonString);
+                        if(userBakConfig != null)
+                        {
+                            userBakConfigs.Add(userBakConfig);
+                        }
+                    }
+                }
+            }
+            list_workspace.ItemsSource = userBakConfigs;
+        }
+
+        private void btn_decrypt_Click(object sender, RoutedEventArgs e)
+        {
+            if(CurrentUserBakConfig != null)
+            {
+                if (!CurrentUserBakConfig.Decrypt)
+                {
+                    byte[]? key = DecryptionHelper.GetWechatKey();
+                    if (key == null)
+                    {
+                        MessageBox.Show("微信密钥获取失败,请检查微信是否打开,或者版本不兼容");
+                        return;
+                    }
+                    string source = Path.Combine(CurrentUserBakConfig.UserWorkspacePath, "OriginalDB");
+                    string to = Path.Combine(CurrentUserBakConfig.UserWorkspacePath, "DecDB");
+                    try
+                    {
+                        WechatDBHelper.DecryUserData(key, source, to);
+                        MessageBox.Show("解密完成,请点击读取数据");
+                        CurrentUserBakConfig.Decrypt = true;
+                        WXWorkspace.SaveConfig(CurrentUserBakConfig);
+                        LoadWorkspace();
+                    }
+                    catch (Exception)
+                    {
+                        MessageBox.Show("解密过程出现错误,请检查是否秘钥是否正确,如果有多开微信,请确保当前微信是选择的用户");
+                    }
+                }
+            }
+        }
+
+        private void btn_read_Click(object sender, RoutedEventArgs e)
+        {
+            if(CurrentUserBakConfig == null)
+            {
+                MessageBox.Show("请先选择工作区");
+                return;
+            }
+            UserReader = new WXUserReader(CurrentUserBakConfig);
+            list_sessions.ItemsSource = UserReader.GetWXContacts();
+        }
+
+        private void list_workspace_SelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            CurrentUserBakConfig = list_workspace.SelectedItem as UserBakConfig;
+            if(CurrentUserBakConfig != null)
+            {
+                user_path.Content = "用户路径:" + CurrentUserBakConfig.UserResPath;
+                if (CurrentUserBakConfig.Decrypt)
+                {
+                    btn_decrypt.IsEnabled = false;
+                    btn_read.IsEnabled = true;
+                }
+                else
+                {
+                    btn_decrypt.IsEnabled = true;
+                    btn_read.IsEnabled = false;
+                }
+            }
+        }
+
+        private void list_sessions_MouseDoubleClick(object sender, MouseButtonEventArgs e)
+        {
+
+        }
+
+        private void Button_Click(object sender, RoutedEventArgs e)
+        {
+            WXContact? wXContact = list_sessions.SelectedItem as WXContact;
+            if(UserReader == null)
+            {
+                MessageBox.Show("请先点击读取已解密工作区");
+                return;
+            }
+            if(wXContact == null || CurrentUserBakConfig == null)
+            {
+                MessageBox.Show("请先选择要导出的联系人");
+                return;
+            }
+
+
+            IExport export = new HtmlExport();
+            export.InitTemplate(wXContact);
+            export.SetMsg(UserReader, wXContact);
+            export.SetEnd();
+            //string path = UserReader.GetSavePath(wXContact);
+            string path = Path.Combine(CurrentUserBakConfig.UserWorkspacePath, wXContact.UserName + ".html");
+            export.Save(path);
+            MessageBox.Show("导出完成");
+
+        }
+
+        private void Button_Click_1(object sender, RoutedEventArgs e)
+        {
+            SelectWechat selectWechat = new SelectWechat();
+            selectWechat.ShowDialog();
+            if(selectWechat.SelectProcess != null)
+            {
+                string path = selectWechat.SelectProcess.DBPath.Replace("\\Msg\\MicroMsg.db", "");
+                try
+                {
+                    WXWorkspace wXWorkspace = new WXWorkspace(path);
+                    wXWorkspace.MoveDB();
+                    MessageBox.Show("创建工作区成功");
+                    LoadWorkspace();
+                }
+                catch (Exception)
+                {
+                    MessageBox.Show("创建工作区失败,请检查路径是否正确");
+                }
+                
+            }
+        }
+
+        private void btn_search_Click(object sender, RoutedEventArgs e)
+        {
+            if(UserReader == null)
+            {
+                MessageBox.Show("请先读取工作区数据");
+                return;
+            }
+            list_sessions.ItemsSource = UserReader.GetWXContacts(find_user.Text);
+        }
+    }
+}

+ 0 - 36
MainWindow.xaml

@@ -1,36 +0,0 @@
-<Window x:Class="WechatPCMsgBakTool.MainWindow"
-        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
-        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
-        xmlns:local="clr-namespace:WechatPCMsgBakTool"
-        mc:Ignorable="d"
-        Title="溯雪PC微信备份工具" Height="450" Width="900">
-    <Grid>
-        <Label Content="用户文件夹:" HorizontalAlignment="Left" Margin="30,27,0,0" VerticalAlignment="Top"/>
-        <TextBox x:Name="txt_user_msg_path" HorizontalAlignment="Left" Margin="110,34,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="400"/>
-        <Button Name="select_user_msg_path" Content="确定" HorizontalAlignment="Left" Width="60" Margin="528,32,0,0" VerticalAlignment="Top" Click="select_user_msg_path_Click"/>
-        <Button x:Name="decryption_user_msg_db" Content="解密" HorizontalAlignment="Left" Width="60" Margin="610,32,0,0" VerticalAlignment="Top" Click="decryption_user_msg_db_Click"/>
-        <Button x:Name="read_user_msg_db" Content="读取" HorizontalAlignment="Left" Width="60" Margin="686,32,0,0" VerticalAlignment="Top" Click="read_user_msg_db_Click"/>
-        <Label Content="会话列表:" HorizontalAlignment="Left" Margin="30,60,0,0" VerticalAlignment="Top"/>
-        <ListView Name="list_sessions" Margin="30,100,0,20" HorizontalAlignment="Left" Width="380" MouseDoubleClick="list_sessions_MouseDoubleClick">
-            <ListView.View>
-                <GridView>
-                    <GridViewColumn Header="昵称" Width="100" DisplayMemberBinding="{Binding NickName}" />
-                    <GridViewColumn Header="原始id" Width="120" DisplayMemberBinding="{Binding UserName}" />
-                    <GridViewColumn Header="最后消息" Width="150" DisplayMemberBinding="{Binding Content}" />
-                </GridView>
-            </ListView.View>
-        </ListView>
-        <Label Content="操作:" HorizontalAlignment="Left" Margin="440,60,0,0" VerticalAlignment="Top"/>
-
-        <Label Content="记录预览:" HorizontalAlignment="Left" Margin="440,150,0,0" VerticalAlignment="Top"/>
-        <ScrollViewer Margin="440,180,20,20" ScrollChanged="ScrollViewer_ScrollChanged" >
-            <Grid Name="msg_list" />
-        </ScrollViewer>
-        <Button x:Name="export_record" Content="导出选中人员记录" HorizontalAlignment="Left" Width="160" Margin="440,105,0,0" VerticalAlignment="Top" Click="export_record_Click" />
-        <TextBox Name="txt_find_session" HorizontalAlignment="Left" Margin="110,67,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="120"/>
-        <Button Name="find_session_person" Content="查找" HorizontalAlignment="Left" Margin="246,67,0,0" VerticalAlignment="Top" Width="70" Click="find_session_person_Click"/>
-        <CheckBox Name="cb_use_local_decdb" Content="使用已解密的工作区读取" HorizontalAlignment="Left" Margin="642,108,0,0" VerticalAlignment="Top"/>
-    </Grid>
-</Window>

+ 0 - 174
MainWindow.xaml.cs

@@ -1,174 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Navigation;
-using System.Windows.Shapes;
-using WechatPCMsgBakTool.Helpers;
-using WechatPCMsgBakTool.Interface;
-using WechatPCMsgBakTool.Model;
-
-namespace WechatPCMsgBakTool
-{
-    /// <summary>
-    /// Interaction logic for MainWindow.xaml
-    /// </summary>
-    public partial class MainWindow : Window
-    {
-        public string UserMsgPath { get; set; } = "";
-        public MainWindow()
-        {
-            InitializeComponent();
-        }
-
-        private void select_user_msg_path_Click(object sender, RoutedEventArgs e)
-        {
-            if (Directory.Exists(txt_user_msg_path.Text))
-            {
-                UserMsgPath = txt_user_msg_path.Text;
-                if (UserMsgPath.Substring(UserMsgPath.Length - 1, 1) == "\\") {
-                    UserMsgPath = UserMsgPath.Substring(0, UserMsgPath.Length - 1);
-                }
-
-                //判定数据目录是否存在
-                if (Directory.Exists(UserMsgPath + "\\Msg"))
-                {
-                    //MessageBox.Show("微信目录存在");
-                }
-
-                //复制数据DB
-                WechatDBHelper.CreateUserWorkPath(UserMsgPath);
-                string err = WechatDBHelper.MoveUserData(UserMsgPath);
-                if(err != "")
-                {
-                    MessageBox.Show(err);
-                    return;
-                }
-                else
-                {
-                    MessageBox.Show("用户目录创建成功,请打开PC微信并登录,获取数据库秘钥解密");
-                }
-            }
-        }
-
-        private void decryption_user_msg_db_Click(object sender, RoutedEventArgs e)
-        {
-            byte[]? key = DecryptionHelper.GetWechatKey();
-            if(key == null)
-            {
-                MessageBox.Show("微信密钥获取失败,请检查微信是否打开,或者版本不兼容");
-                return;
-            }
-            WechatDBHelper.DecryUserData(key);
-            MessageBox.Show("解密完成,请点击读取数据");
-        }
-
-        WXReader? Reader = null;
-        private void read_user_msg_db_Click(object sender, RoutedEventArgs e)
-        {
-            list_sessions.Items.Clear();
-            if (cb_use_local_decdb.IsChecked == true)
-            {
-                DBInfo info = WechatDBHelper.GetDBinfoOnLocal(txt_user_msg_path.Text);
-                Reader = new WXReader(info);
-            }
-            else
-            {
-                Reader = new WXReader();
-            }
-            
-            List<WXSession>? sessions = new List<WXSession>();
-            sessions = Reader.GetWXSessions();
-            if (sessions == null)
-            {
-                MessageBox.Show("咩都厶啊");
-                return;
-            }
-
-            foreach (WXSession session in sessions)
-            {
-                list_sessions.Items.Add(session);
-            }
-        }
-
-        private bool loading = false;
-        private bool end = false;
-        private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
-        {
-            if (sender == null)
-                return;
-            ScrollViewer scrollViewer = (ScrollViewer)sender;
-            if (scrollViewer.ScrollableHeight == 0)
-                return;
-            if (scrollViewer.ScrollableHeight - scrollViewer.ContentVerticalOffset < 10)
-            {
-                if (!loading && !end)
-                {
-                    loading = true;
-                    //GetMsg();
-                }
-            }
-        }
-
-        private void list_sessions_MouseDoubleClick(object sender, MouseButtonEventArgs e)
-        {
-
-        }
-
-        private void export_record_Click(object sender, RoutedEventArgs e)
-        {
-            WXSession selectItem = (WXSession)list_sessions.SelectedValue;
-            if (selectItem != null)
-            {
-                IExport export = new HtmlExport();
-                export.InitTemplate(selectItem);
-
-                if(Reader == null)
-                {
-                    MessageBox.Show("请先读取用户数据");
-                    return;
-                }
-                
-                export.SetMsg(Reader, selectItem);
-                export.SetEnd();
-
-                string path = Reader.GetSavePath(selectItem);
-                export.Save(path);
-                MessageBox.Show("导出完成");
-            }
-        }
-
-        private void find_session_person_Click(object sender, RoutedEventArgs e)
-        {
-            list_sessions.Items.Clear();
-            if (Reader == null)
-                Reader = new WXReader();
-
-            List<WXContact>? sessions = new List<WXContact>();
-            sessions = Reader.GetUser(txt_find_session.Text);
-            if (sessions == null)
-            {
-                MessageBox.Show("咩都厶啊");
-                return;
-            }
-
-            foreach (WXContact session in sessions)
-            {
-                WXSession session1 = new WXSession();
-                session1.NickName = session.NickName;
-                session1.UserName = session.UserName;
-                list_sessions.Items.Add(session1);
-            }
-        }
-    }
-}

+ 6 - 0
Model/Common.cs

@@ -6,6 +6,12 @@ using System.Threading.Tasks;
 
 namespace WechatPCMsgBakTool.Model
 {
+    public class ProcessInfo
+    {
+        public string ProcessName { get; set; } = "";
+        public string ProcessId { get; set; } = "";
+        public string DBPath { get; set; } = "";
+    }
     public class DBInfo
     {
         public int MaxMsgDBCount { get; set; }

+ 17 - 0
Model/WXModel.cs

@@ -4,9 +4,26 @@ using SQLite;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using System.ComponentModel;
 
 namespace WechatPCMsgBakTool.Model
 {
+    public class UserBakConfig : INotifyPropertyChanged
+    {
+        public string UserResPath { get; set; } = "";
+        public string UserWorkspacePath { get; set; } = "";
+        public bool Decrypt { get; set; } = false;
+        public string Hash { get; set; } = "";
+        public string NickName { get; set; } = "";
+        public string UserName { get; set; } = "";
+
+        public event PropertyChangedEventHandler? PropertyChanged;
+        private void OnPropertyChanged(string propertyName)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+        }
+    }
+
     public class WXUserInfo
     {
         public string UserName { get; set; } = "";

+ 10 - 7
README.md

@@ -1,7 +1,7 @@
 # WechatPCMsgBakTool
 微信PC聊天记录备份工具,仅支持Windows
 
-- 当前仅支持3.9.6.33版本,若版本更新可在version.json添加版本号和地址即可完成新版本支持
+- 支持3.9.6.33版本,若版本更新可在version.json添加版本号和地址即可完成新版本支持
 - 导出图片、视频、音频
 - 导出Html文件
 
@@ -11,12 +11,15 @@
 
 #### 使用
 <p>1.打开微信,并登录。</p>
-<p>2.将微信设置内的个人目录,填入用户文件夹的文本框,注意,需要带账号,你从微信设置里面点点打开文件夹那个路径就是对的了。</p>
-<p>3.依次点击,确定,解密,读取,即可在左侧见到会话列表了。</p>
-<p>4.如果会话列表内没有这个人,你可以按账号搜索。</p>
-<p>5.如果使用过程中发生崩溃,请删除工作区试一下,工作区即根据用户名在运行目录下生成的md5文件夹。</p>
-<p>6.如果你已经读取过一次数据了,想离线使用,请填入微信个人目录后,选择使用已解密的工作区读取,即可本地离线加载。</p>
-<p>7.再次强调,仅供个人备份自己微信使用 </p>
+<p>2.在工作区上方点击新增,选择要创建的工作区微信</p>
+<p>3.选中刚刚创建的工作区,点击解密。(如当前多开微信,请选择对应的微信进行解谜)</p>
+<p>4.选中刚刚创建的工作区,点击读取</p>
+<p><b>尽情使用吧!</b></p>
+
+#### 注意
+<p>如果使用过程中发生崩溃,请删除工作区试一下,工作区即根据用户名在运行目录下生成的md5文件夹。</p>
+<p>已解谜的工作区可以直接读取</p>
+<p>再次强调,仅供个人备份自己微信使用 </p>
 
 #### 参考/引用
 都是站在大佬们的肩膀上完成的项目,本项目 参考/引用 了以下 项目/文章 内代码。

+ 24 - 0
SelectWechat.xaml

@@ -0,0 +1,24 @@
+<Window x:Class="WechatPCMsgBakTool.SelectWechat"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:WechatPCMsgBakTool"
+        mc:Ignorable="d"
+        WindowStartupLocation="CenterScreen"
+        Title="选择微信" Height="300" Width="600">
+    <Grid>
+        <Label Content="请选择您要打开的微信:" HorizontalAlignment="Left" Margin="29,27,0,0" VerticalAlignment="Top"/>
+        <ListView Name="list_process" Margin="32,55,32,67" SelectionChanged="list_process_SelectionChanged">
+            <ListView.View>
+                <GridView>
+                    <GridViewColumn Header="进程名" Width="80" DisplayMemberBinding="{Binding ProcessName}" />
+                    <GridViewColumn Header="PID" Width="50" DisplayMemberBinding="{Binding ProcessId}" />
+                    <GridViewColumn Header="路径" Width="300" DisplayMemberBinding="{Binding DBPath}" />
+                </GridView>
+            </ListView.View>
+        </ListView>
+        <Button Name="btn_close" Content="确定并返回" HorizontalAlignment="Left" Margin="240,231,0,0" VerticalAlignment="Top" Width="97" Click="btn_close_Click"/>
+
+    </Grid>
+</Window>

+ 85 - 0
SelectWechat.xaml.cs

@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+using WechatPCMsgBakTool.Model;
+
+namespace WechatPCMsgBakTool
+{
+    /// <summary>
+    /// SelectWechat.xaml 的交互逻辑
+    /// </summary>
+    public partial class SelectWechat : Window
+    {
+        List<ProcessInfo> processInfos = new List<ProcessInfo>();
+        public ProcessInfo? SelectProcess { get; set; } = null;
+        public SelectWechat()
+        {
+            InitializeComponent();
+            GetWechatProcess();
+        }
+
+        public void GetWechatProcess()
+        {
+            Process p = new Process();
+            p.StartInfo.FileName = "tools/handle64.exe";
+            p.StartInfo.Arguments = "-p wechat.exe";
+            p.StartInfo.UseShellExecute = false;
+            p.StartInfo.CreateNoWindow = true;
+            p.StartInfo.RedirectStandardOutput = true;
+            p.Start();
+            string i = p.StandardOutput.ReadToEnd();
+            string[] lines = i.Split(new string[] { "\r\n" }, StringSplitOptions.None);
+            bool hitFind = false;
+            ProcessInfo processInfo = new ProcessInfo();
+            foreach (string line in lines)
+            {
+                if (line.Length < 6)
+                    continue;
+
+                if (line.Substring(0, 6).ToLower() == "wechat")
+                {
+                    hitFind = true;
+                    processInfo = new ProcessInfo();
+                    string[] lineInfo = line.Split(' ');
+                    processInfo.ProcessName = lineInfo[0];
+                    processInfo.ProcessId = lineInfo[2];
+                }
+                if (hitFind)
+                {
+                    if (line.Substring(line.Length - 11, 11) == "MicroMsg.db")
+                    {
+                        Regex regex = new Regex("[a-zA-Z]:\\\\([a-zA-Z0-9() ]*\\\\)*\\w*.*\\w*");
+                        string path = regex.Match(line).Value;
+                        processInfo.DBPath = path;
+                        processInfos.Add(processInfo);
+                        hitFind = false;
+                    }
+                }
+            }
+
+            list_process.ItemsSource = processInfos;
+        }
+
+        private void list_process_SelectionChanged(object sender, SelectionChangedEventArgs e)
+        {
+            SelectProcess = list_process.SelectedItem as ProcessInfo;
+        }
+
+        private void btn_close_Click(object sender, RoutedEventArgs e)
+        {
+            Close();
+        }
+    }
+}

BIN
Tools/handle64.exe


+ 0 - 311
WXReader.cs

@@ -1,311 +0,0 @@
-using SQLite;
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows;
-using System.Windows.Documents;
-using System.Windows.Interop;
-using WechatPCMsgBakTool.Helpers;
-using WechatPCMsgBakTool.Model;
-
-namespace WechatPCMsgBakTool
-{
-    public class WXReader
-    {
-        private DBInfo DecDBInfo;
-        private Dictionary<string, SQLiteConnection> DBInfo = new Dictionary<string, SQLiteConnection>();
-        public WXReader(DBInfo? info = null) {
-            if (info == null)
-                DecDBInfo = WechatDBHelper.GetDBInfo();
-            else
-                DecDBInfo = info;
-
-            string[] dbFileList = Directory.GetFiles(Path.Combine(DecDBInfo.UserPath, "DecDB"));
-            foreach (var item in dbFileList)
-            {
-                FileInfo fileInfo = new FileInfo(item);
-                if (fileInfo.Extension != ".db")
-                    continue;
-                SQLiteConnection con = new SQLiteConnection(item);
-                string dbName = fileInfo.Name.Split('.')[0];
-                DBInfo.Add(dbName, con);
-            }
-        }
-
-        public List<WXSession>? GetWXSessions(string? name = null)
-        {
-            SQLiteConnection con = DBInfo["MicroMsg"];
-            if (con == null)
-                return null;
-            string query = "select * from session";
-            if(name != null)
-            {
-                query = "select * from session where strUsrName = ?";
-                return con.Query<WXSession>(query, name);
-            }
-            return con.Query<WXSession>(query);
-        }
-
-        public List<WXContact>? GetUser(string? name = null)
-        {
-            SQLiteConnection con = DBInfo["MicroMsg"];
-            if (con == null)
-                return null;
-            string query = "select * from contact";
-            if (name != null)
-            {
-                query = "select * from contact where username = ? or alias = ?";
-                return con.Query<WXContact>(query, name, name);
-            }
-            return con.Query<WXContact>(query);
-        }
-
-        public WXSessionAttachInfo? GetWXMsgAtc(WXMsg msg)
-        {
-            SQLiteConnection con = DBInfo["MultiSearchChatMsg"];
-            if (con == null)
-                return null;
-
-            string query = "select * from SessionAttachInfo where msgId = ? order by attachsize desc";
-            List<WXSessionAttachInfo> list = con.Query<WXSessionAttachInfo>(query, msg.MsgSvrID);
-            if (list.Count != 0)
-                return list[0];
-            else
-                return null;
-        }
-
-        public List<WXMsg> GetMsgs(string uid)
-        {
-            List<WXMsg> tmp = new List<WXMsg>();
-            for(int i = 0; i <= DecDBInfo.MaxMsgDBCount; i++)
-            {
-                SQLiteConnection con = DBInfo["MSG" + i.ToString()];
-                if (con == null)
-                    continue;
-
-                string query = "select * from MSG where StrTalker=?";
-                List<WXMsg> wXMsgs = con.Query<WXMsg>(query, uid);
-                foreach(WXMsg w in wXMsgs)
-                {
-                    tmp.Add(w);
-                }
-            }
-            return tmp;
-        }
-
-        public WXMediaMsg? GetVoiceMsg(string msgid)
-        {
-            for (int i = 0; i <= DecDBInfo.MaxMediaDBCount; i++)
-            {
-                SQLiteConnection con = DBInfo["MediaMSG" + i.ToString()];
-                if (con == null)
-                    continue;
-
-                string query = "select * from Media where Reserved0=?";
-                List<WXMediaMsg> wXMsgs = con.Query<WXMediaMsg>(query, msgid);
-                if(wXMsgs.Count != 0)
-                    return wXMsgs[0];
-            }
-            return null;
-        }
-
-        public string? GetVideo(WXMsg msg)
-        {
-            WXSessionAttachInfo? attachInfo = GetWXMsgAtc(msg);
-            if (attachInfo == null)
-                return null;
-
-            string resBasePath = Path.Combine(DecDBInfo.ResPath, attachInfo.attachPath);
-            if (!File.Exists(resBasePath))
-                return null;
-
-            string videoPath = Path.Combine(DecDBInfo.UserPath, msg.StrTalker, "Video");
-            if (!Directory.Exists(videoPath))
-                Directory.CreateDirectory(videoPath);
-
-            FileInfo fileInfo = new FileInfo(resBasePath);
-            string savePath = Path.Combine(videoPath, fileInfo.Name);
-
-            if(!File.Exists(savePath))
-                File.Copy(resBasePath, savePath, false);
-
-            savePath = savePath.Replace(DecDBInfo.UserPath + "\\", "");
-            return savePath;
-        }
-
-        public string? GetVoice(WXMsg msg) { 
-            string tmp = Path.Combine(DecDBInfo.UserPath, msg.StrTalker, "tmp");
-            if (!Directory.Exists(tmp))
-            {
-                Directory.CreateDirectory(tmp);
-            }
-            WXMediaMsg? voiceMsg = GetVoiceMsg(msg.MsgSvrID);
-            if(voiceMsg != null)
-            {
-                if (voiceMsg.Buf == null)
-                    return null;
-
-                string voicePath = Path.Combine(DecDBInfo.UserPath, msg.StrTalker, "Voice");
-                if (!Directory.Exists(voicePath))
-                    Directory.CreateDirectory(voicePath);
-                // 从DB取音频文件到临时目录
-                string tmp_file_path = Path.Combine(tmp, voiceMsg.Key + ".arm");
-                using (FileStream stream = new FileStream(tmp_file_path,FileMode.OpenOrCreate))
-                {
-                    stream.Write(voiceMsg.Buf, 0, voiceMsg.Buf.Length);
-                }
-                // 调用silk_v3_decoder解码成pcm
-                string tmp_pcm_file_path = Path.Combine(tmp, voiceMsg.Key + ".pcm");
-                // 调用ffmpeg转换成mp3
-                string mp3_file_path = Path.Combine(voicePath, voiceMsg.Key + ".mp3");
-                ToolsHelper.DecodeVoice(tmp_file_path, tmp_pcm_file_path, mp3_file_path);
-                mp3_file_path = mp3_file_path.Replace(DecDBInfo.UserPath + "\\", "");
-                return mp3_file_path;
-            }
-            return null; 
-        }
-
-        public string GetSavePath(WXSession session)
-        {
-            string savePath = Path.Combine(DecDBInfo.UserPath, session.UserName + ".html");
-            return savePath;
-        }
-
-        public string? GetImage(WXMsg msg)
-        {
-            WXSessionAttachInfo? attachInfo = GetWXMsgAtc(msg);
-            if (attachInfo == null)
-                return null;
-
-            string resBasePath = Path.Combine(DecDBInfo.ResPath, attachInfo.attachPath);
-
-            //部分attachpath可能会附加md5校验,这里做处理
-            int index = attachInfo.attachPath.IndexOf(".dat");
-            if (attachInfo.attachPath.Length - index > 10)
-            {
-                resBasePath = resBasePath.Substring(0, resBasePath.Length - 32);
-            }
-
-            if (!File.Exists(resBasePath))
-                return null;
-
-            string imgPath = Path.Combine(DecDBInfo.UserPath, msg.StrTalker, "Image");
-            if (!Directory.Exists(imgPath))
-                Directory.CreateDirectory(imgPath);
-
-            string img = DecImage(resBasePath, imgPath);
-            img = img.Replace(DecDBInfo.UserPath + "\\", "");
-            return img;
-        }
-
-        private string DecImage(string source,string toPath)
-        {
-            //读取数据
-            byte[] fileBytes = File.ReadAllBytes(source);
-            //算差异转换
-            byte key = getImgKey(fileBytes);
-            fileBytes = ConvertData(fileBytes, key);
-            //取文件类型
-            string type = CheckFileType(fileBytes);
-            //
-            FileInfo fileInfo = new FileInfo(source);
-            string fileName = fileInfo.Name.Substring(0, fileInfo.Name.Length - 4);
-            string saveFilePath = Path.Combine(toPath, fileName + type);
-            using (FileStream fileStream = File.OpenWrite(saveFilePath))
-            {
-                fileStream.Write(fileBytes, 0, fileBytes.Length);
-                fileStream.Flush();
-            }
-            return saveFilePath;
-        }
-        private string CheckFileType(byte[] data)
-        {
-            switch (data[0])
-            {
-                case 0XFF:  //byte[] jpg = new byte[] { 0xFF, 0xD8, 0xFF };
-                    {
-                        if (data[1] == 0xD8 && data[2] == 0xFF)
-                        {
-                            return ".jpg";
-                        }
-                        break;
-                    }
-                case 0x89:  //byte[] png = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
-                    {
-                        if (data[1] == 0x50 && data[2] == 0x4E && data[7] == 0x0A)
-                        {
-                            return ".png";
-                        }
-                        break;
-                    }
-                case 0x42:  //byte[] bmp = new byte[] { 0x42, 0x4D };
-                    {
-                        if (data[1] == 0X4D)
-                        {
-                            return ".bmp";
-                        }
-                        break;
-                    }
-                case 0x47:  //byte[] gif = new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39(0x37), 0x61 };
-                    {
-                        if (data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x38 && data[5] == 0x61)
-                        {
-                            return ".gif";
-                        }
-                        break;
-                    }
-                case 0x49:  // byte[] tif = new byte[] { 0x49, 0x49, 0x2A, 0x00 };
-                    {
-                        if (data[1] == 0x49 && data[2] == 0x2A && data[3] == 0x00)
-                        {
-                            return ".tif";
-                        }
-                        break;
-                    }
-                case 0x4D:  //byte[] tif = new byte[] { 0x4D, 0x4D, 0x2A, 0x00 };
-                    {
-                        if (data[1] == 0x4D && data[2] == 0x2A && data[3] == 0x00)
-                        {
-                            return ".tif";
-                        }
-                        break;
-                    }
-            }
-
-            return ".dat";
-        }
-        private byte getImgKey(byte[] fileRaw)
-        {
-            byte[] raw = new byte[8];
-            for (int i = 0; i < 8; i++)
-            {
-                raw[i] = fileRaw[i];
-            }
-
-            for (byte key = 0x01; key < 0xFF; key++)
-            {
-                byte[] buf = new byte[8];
-                raw.CopyTo(buf, 0);
-
-                if (CheckFileType(ConvertData(buf, key)) != ".dat")
-                {
-                    return key;
-                }
-            }
-            return 0x00;
-        }
-        private byte[] ConvertData(byte[] data, byte key)
-        {
-            for (int i = 0; i < data.Length; i++)
-            {
-                data[i] ^= key;
-            }
-
-            return data;
-        }
-    }
-}

+ 229 - 0
WXUserReader.cs

@@ -0,0 +1,229 @@
+using SQLite;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Interop;
+using System.Xml.Linq;
+using WechatPCMsgBakTool.Helpers;
+using WechatPCMsgBakTool.Model;
+
+namespace WechatPCMsgBakTool
+{
+    public class WXUserReader
+    {
+        private Dictionary<string, SQLiteConnection> DBInfo = new Dictionary<string, SQLiteConnection>();
+        private UserBakConfig? UserBakConfig = null;
+        public WXUserReader(UserBakConfig userBakConfig) {
+            string path = Path.Combine(userBakConfig.UserWorkspacePath, "DecDB");
+            UserBakConfig = userBakConfig;
+            LoadDB(path);
+        }
+
+        public void LoadDB(string path)
+        {
+            string[] dbFileList = Directory.GetFiles(path);
+            foreach (var item in dbFileList)
+            {
+                FileInfo fileInfo = new FileInfo(item);
+                if (fileInfo.Extension != ".db")
+                    continue;
+                SQLiteConnection con = new SQLiteConnection(item);
+                string dbName = fileInfo.Name.Split('.')[0];
+                DBInfo.Add(dbName, con);
+            }
+        }
+
+        public List<WXContact>? GetWXContacts(string? name = null)
+        {
+            SQLiteConnection con = DBInfo["MicroMsg"];
+            if (con == null)
+                return null;
+            string query = "select * from contact";
+            if (name != null)
+            {
+                query = "select * from contact where username = ? or alias = ?";
+                return con.Query<WXContact>(query, name, name);
+            }
+            return con.Query<WXContact>(query);
+        }
+
+        public List<WXMsg>? GetWXMsgs(string uid)
+        {
+            List<WXMsg> tmp = new List<WXMsg>();
+            for (int i = 0; i <= 99; i++)
+            {
+                if(DBInfo.ContainsKey("MSG" + i.ToString()))
+                {
+                    SQLiteConnection con = DBInfo["MSG" + i.ToString()];
+                    if (con == null)
+                        return tmp;
+
+                    string query = "select * from MSG where StrTalker=?";
+                    List<WXMsg> wXMsgs = con.Query<WXMsg>(query, uid);
+                    foreach (WXMsg w in wXMsgs)
+                    {
+                        tmp.Add(w);
+                    }
+                }
+            }
+            return tmp;
+        }
+
+        public WXSessionAttachInfo? GetWXMsgAtc(WXMsg msg)
+        {
+            SQLiteConnection con = DBInfo["MultiSearchChatMsg"];
+            if (con == null)
+                return null;
+
+            string query = "select * from SessionAttachInfo where msgId = ? order by attachsize desc";
+            List<WXSessionAttachInfo> list = con.Query<WXSessionAttachInfo>(query, msg.MsgSvrID);
+            if (list.Count != 0)
+            {
+                //部分附件可能有md5校验,这里移除校验,给的是正确路径
+                WXSessionAttachInfo acc = list[0];
+                int index = acc.attachPath.IndexOf(".dat");
+                int index2 = acc.attachPath.IndexOf(".dat");
+                if (acc.attachPath.Length - index > 10 && index != -1)
+                {
+                    acc.attachPath = acc.attachPath.Substring(0, acc.attachPath.Length - 32);
+                }
+                if (acc.attachPath.Length - index2 > 10 && index2 != -1)
+                {
+                    acc.attachPath = acc.attachPath.Substring(0, acc.attachPath.Length - 32);
+                }
+                return acc;
+            }
+            else
+                return null;
+        }
+
+        public WXMediaMsg? GetVoiceMsg(WXMsg msg)
+        {
+            for (int i = 0; i <= 99; i++)
+            {
+                if(DBInfo.ContainsKey("MediaMSG" + i.ToString()))
+                {
+                    SQLiteConnection con = DBInfo["MediaMSG" + i.ToString()];
+                    if (con == null)
+                        continue;
+
+                    string query = "select * from Media where Reserved0=?";
+                    List<WXMediaMsg> wXMsgs = con.Query<WXMediaMsg>(query, msg.MsgSvrID);
+                    if (wXMsgs.Count != 0)
+                        return wXMsgs[0];
+                }
+            }
+            return null;
+        }
+
+        public string? GetAttachment(WXMsgType type, WXMsg msg)
+        {
+            if (UserBakConfig == null)
+                return null;
+
+            string? tmpPath = Path.Combine(UserBakConfig.UserWorkspacePath, "Temp");
+            if (!Directory.Exists(tmpPath))
+                Directory.CreateDirectory(tmpPath);
+
+            // 如果是图片和视频,从附件库中搜索
+            string? path = null;
+            if (type == WXMsgType.Image || type == WXMsgType.Video)
+            {
+                WXSessionAttachInfo? atcInfo = GetWXMsgAtc(msg);
+                if (atcInfo == null)
+                    return null;
+                path = atcInfo.attachPath;
+            }
+            // 如果是从语音,从媒体库查找
+            else if (type == WXMsgType.Audio)
+            {
+                WXMediaMsg? voiceMsg = GetVoiceMsg(msg);
+                if (voiceMsg == null)
+                    return null;
+                if (voiceMsg.Buf == null)
+                    return null;
+
+                // 从DB取音频文件到临时目录
+                string tmp_file_path = Path.Combine(tmpPath, voiceMsg.Key + ".arm");
+                using (FileStream stream = new FileStream(tmp_file_path, FileMode.OpenOrCreate))
+                {
+                    stream.Write(voiceMsg.Buf, 0, voiceMsg.Buf.Length);
+                }
+                path = tmp_file_path;
+            }
+
+            if (path == null)
+                return null;
+            
+            // 获取到原路径后,开始进行解密转移,只有图片和语音需要解密,解密后是直接归档目录
+            if(type == WXMsgType.Image || type== WXMsgType.Audio)
+            {
+                path = DecryptAttachment(type, path);
+            }
+            else if (type == WXMsgType.Video)
+            {
+                string video_dir = Path.Combine(UserBakConfig.UserWorkspacePath, "Video");
+                if(!Directory.Exists(video_dir))
+                    Directory.CreateDirectory(video_dir);
+                FileInfo fileInfo = new FileInfo(path);
+                string video_file_path = Path.Combine(video_dir, fileInfo.Name);
+                // 视频的路径是相对路径,需要加上资源目录
+                path = Path.Combine(UserBakConfig.UserResPath, path);
+                if(!File.Exists(video_file_path))
+                    File.Move(path, video_file_path);
+                path = video_file_path;
+            }
+
+            if (path == null)
+                return null;
+
+            // 该相对路径
+            path = path.Replace(UserBakConfig.UserWorkspacePath + "\\", "");
+            return path;
+
+        }
+        public string? DecryptAttachment(WXMsgType type, string path)
+        {
+            if (UserBakConfig == null)
+                return null;
+
+            string? file_path = null;
+            switch (type)
+            {
+                case WXMsgType.Image:
+                    string img_dir = Path.Combine(UserBakConfig.UserWorkspacePath, "Image");
+                    if (!Directory.Exists(img_dir))
+                        Directory.CreateDirectory(img_dir);
+                    // 图片的路径是相对路径,需要加上资源目录
+                    path = Path.Combine(UserBakConfig.UserResPath, path);
+                    byte[] decFileByte = DecryptionHelper.DecImage(path);
+                    string decFiletype = DecryptionHelper.CheckFileType(decFileByte);
+                    file_path = DecryptionHelper.SaveDecImage(decFileByte, path, img_dir, decFiletype);
+                    break;
+                case WXMsgType.Audio:
+                    string audio_dir = Path.Combine(UserBakConfig.UserWorkspacePath, "Audio");
+                    if (!Directory.Exists(audio_dir))
+                        Directory.CreateDirectory(audio_dir);
+                    FileInfo fileInfo = new FileInfo(path);
+                    string audio_file_dir = Path.Combine(audio_dir, fileInfo.Name + ".mp3");
+                    ToolsHelper.DecodeVoice(path, path + ".pcm", audio_file_dir);
+                    file_path = audio_file_dir;
+                    break;
+            }
+            return file_path;
+        }
+    }
+
+    public enum WXMsgType
+    {
+        Image = 0,
+        Video = 1,
+        Audio = 2,
+        File = 3,
+    }
+}

+ 125 - 0
WXWorkspace.cs

@@ -0,0 +1,125 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using WechatPCMsgBakTool.Model;
+
+namespace WechatPCMsgBakTool
+{
+    public class WXWorkspace
+    {
+        private UserBakConfig UserBakConfig = new UserBakConfig();
+        public WXWorkspace(string path) {
+            string checkResult = Init(path);
+            if (checkResult != "")
+                new Exception(checkResult);
+        }
+
+        public WXWorkspace(UserBakConfig userBakConfig)
+        {
+            UserBakConfig = userBakConfig;
+        }
+        public void MoveDB()
+        {
+            string sourceBase = Path.Combine(UserBakConfig.UserResPath, "Msg");
+            string sourceMulit = Path.Combine(UserBakConfig.UserResPath, "Msg/Multi");
+            string[] files = Directory.GetFiles(sourceBase);
+            foreach (string file in files)
+            {
+                FileInfo fileInfo = new FileInfo(file);
+                if(fileInfo.Extension == ".db")
+                {
+                    string to_path = Path.Combine(UserBakConfig.UserWorkspacePath, "OriginalDB", fileInfo.Name);
+                    File.Copy(file, to_path, true);
+                }
+            }
+
+            files = Directory.GetFiles(sourceMulit);
+            foreach (string file in files)
+            {
+                FileInfo fileInfo = new FileInfo(file);
+                if (fileInfo.Extension == ".db")
+                {
+                    string to_path = Path.Combine(UserBakConfig.UserWorkspacePath, "OriginalDB", fileInfo.Name);
+                    File.Copy(file, to_path, true);
+                }
+            }
+        }
+
+        public static void SaveConfig(UserBakConfig userBakConfig)
+        {
+            if(userBakConfig.UserWorkspacePath != "")
+            {
+                DirectoryInfo directoryInfo = new DirectoryInfo(userBakConfig.UserWorkspacePath);
+                if(directoryInfo.Parent != null)
+                {
+                    string json_path = Path.Combine(directoryInfo.Parent.FullName, userBakConfig.UserName + ".json");
+                    string json = JsonConvert.SerializeObject(userBakConfig);
+                    File.WriteAllText(json_path, json);
+                }
+            }
+        }
+        private string Init(string path)
+        {
+            string curPath = AppDomain.CurrentDomain.BaseDirectory;
+            string md5 = GetMd5Hash(path);
+            string[] paths = path.Split(new string[] { "/", "\\" }, StringSplitOptions.None);
+            string username = paths[paths.Length - 1];
+            UserBakConfig.UserResPath = path;
+            UserBakConfig.UserWorkspacePath = Path.Combine(curPath, "workspace", md5);
+            UserBakConfig.Hash = md5;
+            UserBakConfig.UserName = username;
+
+            if (!Directory.Exists(UserBakConfig.UserResPath))
+            {
+                return "用户资源文件夹不存在,如需使用离线数据,请从工作区读取";
+            }
+
+            if (!Directory.Exists(UserBakConfig.UserWorkspacePath))
+            {
+                Directory.CreateDirectory(UserBakConfig.UserWorkspacePath);
+            }
+
+            string db = Path.Combine(UserBakConfig.UserWorkspacePath, "OriginalDB");
+            string decDb = Path.Combine(UserBakConfig.UserWorkspacePath, "DecDB");
+            if (!Directory.Exists(db))
+            {
+                Directory.CreateDirectory (db);
+            }
+            if (!Directory.Exists(decDb))
+            {
+                Directory.CreateDirectory(decDb);
+            }
+            SaveConfig(UserBakConfig);
+            return "";
+        }
+
+        private static string GetMd5Hash(string input)
+        {
+            using (MD5 md5Hash = MD5.Create())
+            {
+                // Convert the input string to a byte array and compute the hash.
+                byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));
+
+                // Create a new Stringbuilder to collect the bytes
+                // and create a string.
+                StringBuilder sBuilder = new StringBuilder();
+
+                // Loop through each byte of the hashed data 
+                // and format each one as a hexadecimal string.
+                for (int i = 0; i < data.Length; i++)
+                {
+                    sBuilder.Append(data[i].ToString("x2"));
+                }
+
+                // Return the hexadecimal string.
+                return sBuilder.ToString();
+            }
+        }
+    }
+}

+ 3 - 0
WechatPCMsgBakTool.csproj

@@ -25,6 +25,9 @@
     <None Update="Tools\ffmpeg.exe">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
+    <None Update="Tools\handle64.exe">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
     <None Update="Tools\silk_v3_decoder.exe">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>

+ 3 - 0
version.json

@@ -2,5 +2,8 @@
 	{
 		"Version": "3.9.6.33",
 		"BaseAddr": 62031872
+	},{
+		"Version":"3.9.7.25",
+		"BaseAddr": 63484032
 	}
 ]