Поиск-фильтр по сообщениям скайпа

    Вчера внезапно узнал, что логи скайпа хранятся в .sqlite. Отлично, подумал я, будет занятие на выходной.
    Сегодня посмотрел хабру, нашел тему, посвященную описанию самой базы — тема, а также по восстановлению этой самой базы — тема и упоминание программки SkypeLogViewer. Замечательно, подумал я, пора писать очередной упоротый велосипед.
    Идея проста: выборка и фильтрация чатов через lua — для тех, кто желает немножко попрактиковаться в использовании lua, sql-запросах и lua-аналога linq, а также тем, кого не устраивает стандартный поиск скайпа. Само приложение написано на C#(WPF).
    Что получилось — смотрите под катом.


    Итак, начнем с простого — подключения библиотек, необходимых для работы с lua и sqlite.

    Выбор пал на NLua и System.Data.Sqlite соответственно. Для установки используем NuGet.

    install-package nlua
    install-package system.data.sqlite

    Для удобства и на всяк пожарный делаем небольшой класс-wrapper для lua

    public class LuaLogic
        {
            public Lua lua = new Lua();
    
            //Использование этой функции позволяет зарегестрировать public - метод класса C# для использования из lua
            public void reg(object target, string funcname)
            {
                try
                {
                    lua.RegisterFunction(funcname, target, target.GetType().GetMethod(funcname));
                }
                catch (Exception ex)
                {
    
                }
            }
    
            //Вызов lua-функции из шарпа
            public object[] call(string lua_func, params object[] args)
            {
                try
                {
                    var func = lua[lua_func] as LuaFunction;
                    return func.Call(args);
                }
                catch (Exception ex)
                {
                    return null;
                }
            }
        }
    


    И да, я в курсе, что многие считают, что Exception обязан выводиться — вот только надобности в этом в данном конкретном случае не вижу.

    Разметку для gui выкладывать не буду, потому вот описание используемых в коде элементов GUI:

    output — RichTextBox, для вывода разного рода информации, к примеру, пошлых шуток или ascii-арта
    runlua — Button, для выполнения lua-кода. Вообще-то можно подвесить на изменение файла при помощи FileSystemWatcher'а, но это уже на любителя
    accounts — ComboBox, в который будет выводиться список пользователей скайпа, когда-либо логинившихся на компьютере

    Теперь перейдем, собственно, к коду. Начнем со вспомогательных функций.

    
            static LuaLogic logic = new LuaLogic();
            public string current_path = "";
            private List<Dictionary<string, object>> data;
    
    
            //Выполнение запроса к базе. Возвращает данные в более-менее удобном для работы формате.
            private List<Dictionary<string, object>> _query(string comm)
            {
                var result = new List<Dictionary<string, object>>();
                using (var db = new SQLiteConnection(@"data source=" + current_path))
                {
                    db.Open();
                    using (var command = new SQLiteCommand(comm, db))
                    {
                        command.CommandTimeout = 999;
                        using (var reader = command.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                result.Add(Enumerable.Range(0, reader.FieldCount)
                                    .ToDictionary(
                                        reader.GetName,
                                        reader.GetValue));
                            }
                        }
                    }
                    db.Close();
                }
                return result;
            }
    
    
            //Функция для упаковки результата выборки в понятный lua формат. К сожалению, как работать с List<Dictionary<string, object>> через lua так и не допер. Преобразовывает коллекцию в lua-таблицу, состоящуюю из lua-таблиц.
            public LuaTable genTable(List<Dictionary<string, object>> d)
            {
                logic.lua.NewTable("datatable");
                var table = logic.lua.GetTable("datatable");
                for(int i=0; i<d.Count; i++)
                {
                    logic.lua.NewTable("f");
                    table[i] = logic.lua.GetTable("f");
                    foreach (var entry in d[i])
                    {
                        ((LuaTable) table[i])[entry.Key] = entry.Value;
                    }
                }
                return table;
            }
    
    
            /*** Функции, вызываемые из lua ***/
    
            //Собственно, запрос к БД. Вызов перенес на сторону lua для пущего удобства.
            public void scanDB(string request=null)
            {
                if(data!=null)
                    data.Clear();
    
                data = new List<Dictionary<string, object>>();
                data = _query(request??"select from_dispname,body_xml,timestamp from messages order by timestamp desc");
                genTable(data);
            }
    
            //Вывод данных в RichTextBox
            public void _print(object obj)
            {
                Dispatcher.Invoke(() =>output.AppendText(obj + "\n"));
            }
    
            //Еще 1 вариант вывода данных в RichTextBox. Просто так.
            public void _printblock(string text)
            {
                Dispatcher.Invoke(() => 
                output.Document.Blocks.Add(new Paragraph(new Run(text))
                                               {
                                                   Margin = new Thickness(0)
                                               }));
            }
    
            //Очистка RichTextBox
            public void _clear()
            {
                Dispatcher.Invoke(() => output.Document.Blocks.Clear());
            }
    


    А теперь — логика приложения!

    public MainWindow()
            {
                InitializeComponent();
    
                //Получаем путь до настроек скайпа. Если у вас лежит в другом месте - ну что ж, допилку лобзиком никто не отменял
                var searchpath = Environment.ExpandEnvironmentVariables("%AppData%\\Skype");
                var dirs = Directory.GetDirectories(searchpath);
                //Немного стремный способ получения папок со списком юзеров, но умнее и универсальнее не придумал
                var userlist = dirs.Where(dir => File.Exists(dir + "\\main.db")).Select(x=>x.Replace(searchpath+"\\", "")).ToList();
    
                accounts.ItemsSource = userlist;
                accounts.SelectedItem = accounts.Items[0];
    
                //меняем путь до файла с логами по изменению выбранного значения в ComboBox'е
                accounts.SelectionChanged += (sender, args) => current_path = Environment.ExpandEnvironmentVariables("%AppData%\\Skype") + "\\" + accounts.SelectedItem + "\\main.db";
    
                if(userlist.Count>0)
                     current_path = Environment.ExpandEnvironmentVariables("%AppData%\\Skype") + "\\" + userlist[0] + "\\main.db";
                else
                {
                     _print("Зачем тебе читалка логов скайпа, если у тебя даже скайпа нету?");
                }
    
                //Регистрируем шарповские функции для вызова из Lua
                logic.reg(this, "_print");
                logic.reg(this, "_clear");
                logic.reg(this, "_printblock");
                logic.reg(this, "scanDB");
                
    
    
                runlua.Click += (sender, args) =>
                {
                    try
                    {
                        new Thread(() =>
                                       {
                                           //Подключаем 2 скрипта- для работы с linq-подобными where и select и, собственно, основной скрипт. Lua-linq я честно спер и переделал под свои задачи <a href="http://codea.io/talk/discussion/618/linq-for-lua-functional-collection-class/p1">отсюда</a>.
                                           logic.lua.DoFile(@"scripts\pseudolinq.lua");
                                           logic.lua.DoFile(@"scripts\script.lua");
    
                                           //Вызов функции, в которой хранится lua-логика. Кстати, необязательно, достаточно добавить вызов нужной функции в одном из подгружаемых lua-скриптов
                                           logic.call("search_pattern");
                                       }).Start();
                    }
                    catch (Exception ex)
                    {
                        _printblock(ex.Message);
                    }
                };
    
                //Старая добрая заглушка, на случай, если нету необходимости сохранения данных и лень возиться с потоками
                Closing += (sender, args) => Process.GetCurrentProcess().Kill();
            }
    


    Ну и на закуску — lua-код.

    pseudolinq.lua
    LinqArray = {}
    
    function LinqArray:new( arr )
    		Ret = {}
    		Ret.arr = arr;
    		setmetatable( Ret , self )
    		self.__index = self;
    		return Ret;
    end
    
    --[[function LinqArray:init(items)
    	if items then self:addRange(items) end
    end]]--
    
    function LinqArray:add(item)
    table.insert(self.arr, item)
    end
    
    function LinqArray:addRange(items)
    for k,v in ipairs(items) do
    self.arr:add(v)
    end
    end
    
    function LinqArray:where(func)
    local results = {};
    for k, v in ipairs(self.arr) do
    	if func(v) then
    		table.insert(results, v);
    	end
    end
    
    return LinqArray:new(results)
    end
    
    function LinqArray:select(func)
    local results = {}
    for k, v in ipairs(self.arr) do
    	_print(func(v));
    	table.insert(results, func(v));
    end
    
    return LinqArray:new(results)
    end
    


    script.lua

    function search_pattern()		
            --подчищаем вывод от предыдущего запроса
    	_clear();
    	local f = LinqArray:new(datatable);
    
            --самая мякотка - linq-подобный фильтр
    	local filtered = f:where(function(x) return string.len(x["from_dispname"])>1; end):select(function(x) return x["from_dispname"]; end);
    	
            --распечатка полученных сообщений в нужном формате. Просто пример. Использование именно в таком виде необязательно, нежелаемо и вообще, нерекомендуемо. Если что - я предупредил.
    
    	local i=0;
    	for i=1,#filtered.arr,1 do
    		local arg = filtered.arr[i];	
    		 _printblock(arg["from_dispname"]..": ");
    		 _print(arg["body_xml"]);
    	end
    end
    
    --Собственно, запрос к базе. Результат хранится в памяти для ускорения работы. В данном случае, запрос выполняется только 1 раз, что бы выполнять при каждом перезапуске lua, достаточно убрать проверку
    if(datatable==nil) then
    		scanDB("select * from messages limit 100");
    end
    


    Остановлюсь чуть поподробнее на фильтре.
    Вообще, делать его в linq-подобном формате необязательно, да и сам фильтр можно было делать запросом — но это же хаб «ненормальное программирование», нужно же добавить что-нибудь неочевидное.
    Остановка завершена.

    В принципе, это все.
    Спасибо за внимание!
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 4
    • +3
      Качество lua кода удручает :(
      • 0
        Чем именно?
        • +3
          • Полное отсутствие какого-либо code style;
          • Объявление глобальных переменных (LinqArray = {}, Ret = {});
          • Работа с self.arr:add(v), хотя add объявлен для самого self, и это работает лишь благодаря инициализации в LinqArray:new;
          • Реализация «linq-подобного фильтра» будет короче и понятнее на чистом lua;
          • Реализуете обертку, а потом выставляете наружу «кишки»: local arg = filtered.arr[i]; и #filtered.arr.

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.