Вчера внезапно узнал, что логи скайпа хранятся в .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