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

в 18:13, , рубрики: Lua, sqlite, ненормальное программирование

Вчера внезапно узнал, что логи скайпа хранятся в .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(@"scriptspseudolinq.lua");
                                       logic.lua.DoFile(@"scriptsscript.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-подобном формате необязательно, да и сам фильтр можно было делать запросом — но это же хаб «ненормальное программирование», нужно же добавить что-нибудь неочевидное.
Остановка завершена.

В принципе, это все.
Спасибо за внимание!

Автор: Demogor

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js