diff --git a/src/lib/qutim/history.cpp b/src/lib/qutim/history.cpp index 4f99dc260..0da282be3 100644 --- a/src/lib/qutim/history.cpp +++ b/src/lib/qutim/history.cpp @@ -49,10 +49,6 @@ namespace qutim_sdk_0_3 return AsyncResult::create(MessageList()); } - void showHistory(const ChatUnit *) override - { - } - AsyncResult> accounts() override { return AsyncResult>::create(QVector()); @@ -63,12 +59,12 @@ namespace qutim_sdk_0_3 return AsyncResult>::create(QVector()); } - AsyncResult> months(const ContactInfo &, const QRegularExpression &) override + AsyncResult> months(const ContactInfo &, const QString &) override { return AsyncResult>::create(QList()); } - AsyncResult> dates(const ContactInfo &, const QDate &, const QRegularExpression &) override + AsyncResult> dates(const ContactInfo &, const QDate &, const QString &) override { return AsyncResult>::create(QList()); } diff --git a/src/lib/qutim/history.h b/src/lib/qutim/history.h index 1fe4ac69e..b777f8d5b 100644 --- a/src/lib/qutim/history.h +++ b/src/lib/qutim/history.h @@ -34,6 +34,9 @@ namespace qutim_sdk_0_3 { class ChatUnit; + /** + * @brief The History class is base class to implement history storage plugins + */ class LIBQUTIM_EXPORT History : public QObject { Q_OBJECT @@ -61,11 +64,24 @@ namespace qutim_sdk_0_3 }; virtual void store(const Message &message) = 0; + /** + * Read messages from history. + * \warning \a from or/and \a to parameter can be invalid, which means that + * you should ignore that parameter + */ virtual AsyncResult read(const ContactInfo &contact, const QDateTime &from, const QDateTime &to, int max_num) = 0; virtual AsyncResult> accounts() = 0; virtual AsyncResult> contacts(const AccountInfo &account) = 0; - virtual AsyncResult> months(const ContactInfo &contact, const QRegularExpression ®ex) = 0; - virtual AsyncResult> dates(const ContactInfo &contact, const QDate &month, const QRegularExpression ®ex) = 0; + /** + * Returns list of date(year, month). If search string is presented, you may return list of months where + * at least message contains search string. + */ + virtual AsyncResult> months(const ContactInfo &contact, const QString &needle) = 0; + /** + * Returns list of days in month of certain year. If search string is not empty, you should return list of days + * where at least one message with search string is presented + */ + virtual AsyncResult> dates(const ContactInfo &contact, const QDate &month, const QString &needle) = 0; AsyncResult read(const ChatUnit *unit, const QDateTime &to, int max_num); AsyncResult read(const ChatUnit *unit, int max_num); @@ -75,9 +91,6 @@ namespace qutim_sdk_0_3 static ContactInfo info(const ChatUnit *unit); - public slots: - virtual void showHistory(const ChatUnit *unit) = 0; - protected: History(); diff --git a/src/plugins/generic/generic.qbs b/src/plugins/generic/generic.qbs index c709f9933..a1dd6dadb 100644 --- a/src/plugins/generic/generic.qbs +++ b/src/plugins/generic/generic.qbs @@ -32,6 +32,7 @@ Project { "formula/formula.qbs", "highlighter/highlighter.qbs", "histman/histman.qbs", + "histview/histview.qbs", "hunspeller/hunspeller.qbs", "idledetector/idledetector.qbs", "idlestatuschanger/idlestatuschanger.qbs", @@ -75,6 +76,7 @@ Project { "simplecontactlist/simplecontactlist.qbs", "simplerosterstorage/simplerosterstorage.qbs", "soundthemeselector/soundthemeselector.qbs", + "sqlitehistory/sqlitehistory.qbs", "trayicon/trayicon.qbs", "unreadmessageskeeper/unreadmessageskeeper.qbs", "updater/updater.qbs", diff --git a/src/plugins/generic/histview/histview.plugin.json b/src/plugins/generic/histview/histview.plugin.json new file mode 100644 index 000000000..de5feb5ab --- /dev/null +++ b/src/plugins/generic/histview/histview.plugin.json @@ -0,0 +1,7 @@ +{ + "pluginIcon": "", + "pluginName": "History View", + "pluginDescription": "Simple separate window plugin for viewing history", + "extensionHeader": "src/histview.h", + "extensionClass": "Core::HistView" +} diff --git a/src/plugins/generic/histview/histview.qbs b/src/plugins/generic/histview/histview.qbs new file mode 100644 index 000000000..89b40c682 --- /dev/null +++ b/src/plugins/generic/histview/histview.qbs @@ -0,0 +1,5 @@ +import "../GenericPlugin.qbs" as GenericPlugin + +GenericPlugin { + +} diff --git a/src/plugins/generic/jsonhistory/historywindow.cpp b/src/plugins/generic/histview/src/historywindow.cpp similarity index 90% rename from src/plugins/generic/jsonhistory/historywindow.cpp rename to src/plugins/generic/histview/src/historywindow.cpp index 55fbe4ef2..3fffaa9b1 100644 --- a/src/plugins/generic/jsonhistory/historywindow.cpp +++ b/src/plugins/generic/histview/src/historywindow.cpp @@ -69,7 +69,7 @@ HistoryWindow::HistoryWindow(const ChatUnit *unit) ui.setupUi(this); ui.historyLog->setHtml("

" - + tr("No History") + "

"); + + tr("No History") + "

"); ui.label_in->setText( tr( "In: %L1").arg( 0 ) ); ui.label_out->setText( tr( "Out: %L1").arg( 0 ) ); ui.label_all->setText( tr( "All: %L1").arg( 0 ) ); @@ -92,6 +92,8 @@ HistoryWindow::HistoryWindow(const ChatUnit *unit) connect(ui.dateTreeWidget, &QTreeWidget::itemExpanded, this, &HistoryWindow::fillMonth); + connect(ui.dateTreeWidget, &QTreeWidget::itemCollapsed, this, &HistoryWindow::clearMonth); + fillAccountComboBox(); setParent(QApplication::activeWindow()); @@ -126,7 +128,21 @@ void HistoryWindow::setUnit(const ChatUnit *unit) void HistoryWindow::setIcons() { // setWindowIcon(Icon("history")); -// ui.searchButton->setIcon(Icon("search")); + // ui.searchButton->setIcon(Icon("search")); +} + +QTreeWidgetItem *HistoryWindow::findChild(QTreeWidgetItem *parent, const QVariant &value) +{ + if (!parent) + return nullptr; + + for (int i = 0; i < parent->childCount(); ++i) { + QTreeWidgetItem *child = parent->child(i); + if (child->data(0, Qt::UserRole) == value) + return child; + } + + return nullptr; } void HistoryWindow::fillAccountComboBox() @@ -146,10 +162,14 @@ void HistoryWindow::fillAccountComboBox() connect(ui.accountComboBox, static_cast(&QComboBox::currentIndexChanged), this, &HistoryWindow::fillContactComboBox); int accountIndex = ui.accountComboBox->findData(QVariant::fromValue(m_unitInfo)); + if (accountIndex < 0) fillContactComboBox(0); - else + else { ui.accountComboBox->setCurrentIndex(accountIndex); + fillContactComboBox(accountIndex); + } + }); } @@ -201,7 +221,7 @@ void HistoryWindow::fillDateTreeWidget(int index) setWindowTitle(QStringLiteral("%1 (%2)").arg(ui.fromComboBox->currentText(), ui.accountComboBox->currentText())); - history()->months(contactInfo, m_search).connect(this, [this, contactInfo] (const QList &months) { + history()->months(contactInfo, m_search_word).connect(this, [this, contactInfo] (const QList &months) { int index = ui.fromComboBox->currentIndex(); auto currentContactInfo = ui.fromComboBox->itemData(index).value(); if (!(currentContactInfo == contactInfo)) @@ -244,25 +264,12 @@ void HistoryWindow::fillMonth(QTreeWidgetItem *monthItem) auto contactInfo = ui.fromComboBox->itemData(contactIndex).value(); auto month = monthItem->data(0, Qt::UserRole).toDate(); - history()->dates(contactInfo, month, m_search).connect(this, [this, contactInfo, month] (const QList &dates) { + history()->dates(contactInfo, month, m_search_word).connect(this, [this, contactInfo, month] (const QList &dates) { int contactIndex = ui.fromComboBox->currentIndex(); auto currentContactInfo = ui.fromComboBox->itemData(contactIndex).value(); if (!(currentContactInfo == contactInfo)) return; - auto findChild = [] (QTreeWidgetItem *parent, const QVariant &value) -> QTreeWidgetItem * { - if (!parent) - return nullptr; - - for (int i = 0; i < parent->childCount(); ++i) { - QTreeWidgetItem *child = parent->child(i); - if (child->data(0, Qt::UserRole) == value) - return child; - } - - return nullptr; - }; - auto monthItem = findChild(findChild(ui.dateTreeWidget->invisibleRootItem(), month.year()), month); if (!monthItem) return; @@ -276,6 +283,18 @@ void HistoryWindow::fillMonth(QTreeWidgetItem *monthItem) }); } +void HistoryWindow::clearMonth(QTreeWidgetItem *monthItem) { + auto month = monthItem->data(0, Qt::UserRole).toDate(); + + auto item = findChild(findChild(ui.dateTreeWidget->invisibleRootItem(), month.year()), month); + + if(item) { + auto items = item->takeChildren(); + qDeleteAll(items); + } +} + + void HistoryWindow::on_dateTreeWidget_currentItemChanged(QTreeWidgetItem *dayItem, QTreeWidgetItem *) { QTreeWidgetItem *monthItem = dayItem ? dayItem->parent() : nullptr; @@ -351,7 +370,11 @@ void HistoryWindow::on_dateTreeWidget_currentItemChanged(QTreeWidgetItem *dayIte cursor.insertHtml(historyMessage); cursor.insertText(QStringLiteral("\n")); } else { - cursor.insertHtml(historyMessage.replace(m_search, resultString)); + QRegularExpression expr; + expr.setPattern(QLatin1Char('(') + QRegularExpression::escape(m_search_word) + QLatin1Char(')')); + expr.setPatternOptions(QRegularExpression::MultilineOption | QRegularExpression::CaseInsensitiveOption); + + cursor.insertHtml(historyMessage.replace(expr, resultString)); cursor.insertText(QStringLiteral("\n")); } } @@ -382,8 +405,6 @@ void HistoryWindow::on_searchButton_clicked() } } else { m_search_word = searchWord; - m_search.setPattern(QLatin1Char('(') + QRegularExpression::escape(searchWord) + QLatin1Char(')')); - m_search.setPatternOptions(QRegularExpression::MultilineOption | QRegularExpression::CaseInsensitiveOption); fillDateTreeWidget(ui.fromComboBox->currentIndex()); } } diff --git a/src/plugins/generic/jsonhistory/historywindow.h b/src/plugins/generic/histview/src/historywindow.h similarity index 94% rename from src/plugins/generic/jsonhistory/historywindow.h rename to src/plugins/generic/histview/src/historywindow.h index 5404fbebe..c14a105d4 100644 --- a/src/plugins/generic/jsonhistory/historywindow.h +++ b/src/plugins/generic/histview/src/historywindow.h @@ -38,7 +38,6 @@ using namespace qutim_sdk_0_3; namespace Core { -class JsonEngine; class HistoryWindow : public QWidget { @@ -52,6 +51,7 @@ private slots: void fillContactComboBox(int index); void fillDateTreeWidget(int index); void fillMonth(QTreeWidgetItem *month); + void clearMonth(QTreeWidgetItem *month); void on_dateTreeWidget_currentItemChanged( QTreeWidgetItem* current, QTreeWidgetItem* previous ); void on_searchButton_clicked(); void findPrevious(); @@ -62,8 +62,8 @@ private slots: Ui::HistoryWindowClass ui; QMetaObject::Connection m_contactConnection; History::ContactInfo m_unitInfo; - QRegularExpression m_search; QString m_search_word; + QTreeWidgetItem* findChild(QTreeWidgetItem *parent, const QVariant &value); }; } diff --git a/src/plugins/generic/jsonhistory/historywindow.ui b/src/plugins/generic/histview/src/historywindow.ui similarity index 85% rename from src/plugins/generic/jsonhistory/historywindow.ui rename to src/plugins/generic/histview/src/historywindow.ui index 2251d2a5f..f82306f3f 100644 --- a/src/plugins/generic/jsonhistory/historywindow.ui +++ b/src/plugins/generic/histview/src/historywindow.ui @@ -14,7 +14,16 @@ HistoryWindow - + + 4 + + + 4 + + + 4 + + 4 @@ -40,10 +49,18 @@ - + + + QComboBox::AdjustToContents + + - + + + QComboBox::AdjustToContents + + diff --git a/src/plugins/generic/histview/src/histview.cpp b/src/plugins/generic/histview/src/histview.cpp new file mode 100644 index 000000000..ab18df1da --- /dev/null +++ b/src/plugins/generic/histview/src/histview.cpp @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** qutIM - instant messenger +** +** Copyright © 2015 Nicolay Izoderov +** +***************************************************************************** +** +** $QUTIM_BEGIN_LICENSE$ +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +** See the GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see http://www.gnu.org/licenses/. +** $QUTIM_END_LICENSE$ +** +****************************************************************************/ + +#include "histview.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Core +{ + +HistView::HistView() +{ + m_historyAction = new ActionGenerator(Icon("view-history"), + QT_TRANSLATE_NOOP("Chat", "View History"), + this, + SLOT(onHistoryActionTriggered(QObject*))); + m_historyAction->setType(ActionTypeChatButton|ActionTypeContactList); + m_historyAction->setPriority(512); + MenuController::addAction(m_historyAction); +} + +HistView::~HistView() +{ + delete m_historyAction; +} + +void HistView::onHistoryActionTriggered(QObject* object) +{ + ChatUnit *unit = qobject_cast(object); + Q_ASSERT(unit); + + showHistory(unit); +} + +void HistView::showHistory(const ChatUnit *unit) +{ + unit = unit->getHistoryUnit(); + if (m_historyWindow) { + m_historyWindow.data()->setUnit(unit); + m_historyWindow.data()->raise(); + } else { + m_historyWindow = new HistoryWindow(unit); + m_historyWindow.data()->show(); + } +} + +} diff --git a/src/plugins/generic/histview/src/histview.h b/src/plugins/generic/histview/src/histview.h new file mode 100644 index 000000000..27dd3cb96 --- /dev/null +++ b/src/plugins/generic/histview/src/histview.h @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** qutIM - instant messenger +** +** Copyright © 2008 Rustam Chakin +** Copyright © 2011 Ruslan Nigmatullin +** Copyright © 2015 Nicolay Izoderov +** +***************************************************************************** +** +** $QUTIM_BEGIN_LICENSE$ +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +** See the GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see http://www.gnu.org/licenses/. +** $QUTIM_END_LICENSE$ +** +****************************************************************************/ + +#ifndef HISTVIEW_H +#define HISTVIEW_H + +#include +#include +#include +#include +#include +#include "historywindow.h" + +using namespace qutim_sdk_0_3; + +namespace Core +{ + +class HistView : public QObject +{ + Q_OBJECT + Q_CLASSINFO("Service", "HistView") +public: + HistView(); + ~HistView(); +public slots: + void onHistoryActionTriggered(QObject *object); + +private: + QPointer m_historyWindow; + ActionGenerator* m_historyAction; + void showHistory(const ChatUnit *unit); +}; + +} + +#endif // HISTVIEW_H diff --git a/src/plugins/generic/jsonhistory/jsonhistory.cpp b/src/plugins/generic/jsonhistory/jsonhistory.cpp index 9ae6c087b..6074def3f 100644 --- a/src/plugins/generic/jsonhistory/jsonhistory.cpp +++ b/src/plugins/generic/jsonhistory/jsonhistory.cpp @@ -31,7 +31,6 @@ #include #include #include -#include "historywindow.h" #include #include //#include @@ -75,25 +74,8 @@ static void runJob(JsonHistoryScope::Ptr scope, Method method) } } -void init(History *history) -{ - ActionGenerator *gen = new ActionGenerator(Icon("view-history"), - QT_TRANSLATE_NOOP("Chat", "View History"), - history, - SLOT(onHistoryActionTriggered(QObject*))); - gen->setType(ActionTypeChatButton|ActionTypeContactList); - gen->setPriority(512); - MenuController::addAction(gen); -} - JsonHistory::JsonHistory() : m_scope(new JsonHistoryScope) { - static bool inited = false; - if (!inited) { - inited = true; - init(this); - } - m_scope->hasJobRunnable = false; } @@ -396,9 +378,9 @@ AsyncResult> JsonHistory::contacts(const AccountIn return handler.result(); } -AsyncResult> JsonHistory::months(const ContactInfo &contact, const QRegularExpression ®ex) +AsyncResult> JsonHistory::months(const ContactInfo &contact, const QString &needle) { - Q_UNUSED(regex); + Q_UNUSED(needle); AsyncResultHandler> handler; auto scope = m_scope; @@ -430,12 +412,12 @@ AsyncResult> JsonHistory::months(const ContactInfo &contact, const return handler.result(); } -AsyncResult> JsonHistory::dates(const ContactInfo &contact, const QDate &month, const QRegularExpression ®ex) +AsyncResult> JsonHistory::dates(const ContactInfo &contact, const QDate &month, const QString &needle) { AsyncResultHandler> handler; auto scope = m_scope; - runJob(m_scope, [handler, scope, contact, month, regex] () { + runJob(m_scope, [handler, scope, contact, month, needle] () { QSet result; QFile file(scope->getFileName(contact, month)); @@ -479,7 +461,7 @@ AsyncResult> JsonHistory::dates(const ContactInfo &contact, const Q const QDate date = QDateTime::fromString(message.value("datetime").toString(), Qt::ISODate).date(); const QString text = message.value("text").toString(); - if (!regex.isValid() || text.contains(regex)) + if (needle.isEmpty() || text.contains(needle, Qt::CaseInsensitive)) result.insert(date); } } @@ -493,18 +475,6 @@ AsyncResult> JsonHistory::dates(const ContactInfo &contact, const Q return handler.result(); } -void JsonHistory::showHistory(const ChatUnit *unit) -{ - unit = unit->getHistoryUnit(); - if (m_historyWindow) { - m_historyWindow.data()->setUnit(unit); - m_historyWindow.data()->raise(); - } else { - m_historyWindow = new Core::HistoryWindow(unit); - m_historyWindow.data()->show(); - } -} - QString JsonHistory::quote(const QString &str) { const static bool true_chars[128] = @@ -561,11 +531,5 @@ QString JsonHistory::unquote(const QString &str) return result; } -void JsonHistory::onHistoryActionTriggered(QObject* object) -{ - ChatUnit *unit = qobject_cast(object); - showHistory(unit); -} - } diff --git a/src/plugins/generic/jsonhistory/jsonhistory.h b/src/plugins/generic/jsonhistory/jsonhistory.h index 6ce8c8ef5..76beb7ff2 100644 --- a/src/plugins/generic/jsonhistory/jsonhistory.h +++ b/src/plugins/generic/jsonhistory/jsonhistory.h @@ -38,8 +38,6 @@ using namespace qutim_sdk_0_3; namespace Core { -class HistoryWindow; - class JsonHistoryScope { public: @@ -86,18 +84,14 @@ class JsonHistory : public History AsyncResult read(const ContactInfo &info, const QDateTime &from, const QDateTime &to, int max_num) override; AsyncResult> accounts() override; AsyncResult> contacts(const AccountInfo &account) override; - AsyncResult> months(const ContactInfo &contact, const QRegularExpression ®ex) override; - AsyncResult> dates(const ContactInfo &contact, const QDate &month, const QRegularExpression ®ex) override; - void showHistory(const ChatUnit *unit) override; + AsyncResult> months(const ContactInfo &contact, const QString &needle) override; + AsyncResult> dates(const ContactInfo &contact, const QDate &month, const QString &needle) override; static QString quote(const QString &str); static QString unquote(const QString &str); -private slots: - void onHistoryActionTriggered(QObject *object); private: JsonHistoryScope::Ptr m_scope; - QPointer m_historyWindow; }; } diff --git a/src/plugins/generic/sqlitehistory/sqlitehistory.cpp b/src/plugins/generic/sqlitehistory/sqlitehistory.cpp new file mode 100644 index 000000000..b2893a23a --- /dev/null +++ b/src/plugins/generic/sqlitehistory/sqlitehistory.cpp @@ -0,0 +1,515 @@ +/**************************************************************************** +** +** qutIM - instant messenger +** +** Copyright © 2015 Nicolay Izoderov +** +***************************************************************************** +** +** $QUTIM_BEGIN_LICENSE$ +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +** See the GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see http://www.gnu.org/licenses/. +** $QUTIM_END_LICENSE$ +** +****************************************************************************/ + +#include "sqlitehistory.h" +#include +#include +#include +#include +#include +#include +#include +#include +//#include +#include +#include +#include +#include + +namespace Core +{ + +SqliteHistory::SqliteHistory() : + m_thread(new QThread), + m_worker(new SqliteWorker) +{ + m_worker->moveToThread(m_thread); + + connect(m_worker, SIGNAL(error(QString)), this, SLOT(errorHandler(QString))); + + connect(m_thread, SIGNAL(started()), m_worker, SLOT(process())); + + connect(m_worker, SIGNAL(finished()), m_thread, SLOT(quit())); + connect(m_worker, SIGNAL(finished()), m_worker, SLOT(deleteLater())); + connect(m_thread, SIGNAL(finished()), m_thread, SLOT(deleteLater())); + + m_thread->start(); +} + +SqliteHistory::~SqliteHistory() +{ + m_worker->shutdown(); + m_thread->wait(); + + delete m_worker; + delete m_thread; +} + +void SqliteHistory::store(const Message &message) +{ + if (!message.chatUnit()) + return; + + auto contactInfo = info(message.chatUnit()); + + m_worker->runJob([message, contactInfo] () { + QSqlQuery query; + + query.prepare(QStringLiteral("INSERT INTO qutim_history (account, protocol, contact, year, month, day, time, incoming, message, html, type, sendername) " + "VALUES (:account, :protocol, :contact, :year, :month, :day, :time, :incoming, :message, :html, :type, :sendername)")); + + query.bindValue(QStringLiteral(":account"), contactInfo.account); + query.bindValue(QStringLiteral(":protocol"), contactInfo.protocol); + query.bindValue(QStringLiteral(":contact"), contactInfo.contact); + + query.bindValue(QStringLiteral(":year"), message.time().date().year()); + query.bindValue(QStringLiteral(":month"), message.time().date().month()); + query.bindValue(QStringLiteral(":day"), message.time().date().day()); + query.bindValue(QStringLiteral(":time"), message.time().toTime_t()); + + query.bindValue(QStringLiteral(":incoming"), message.isIncoming()); + query.bindValue(QStringLiteral(":message"), message.text()); + query.bindValue(QStringLiteral(":html"), message.html()); + + SqliteWorker::MessageTypes type = SqliteWorker::Message; + if(message.property("topic", false)) + type |= SqliteWorker::Topic; + if(message.property("service", false)) + type |= SqliteWorker::Service; + + query.bindValue(QStringLiteral(":type"), static_cast(type)); + query.bindValue(QStringLiteral(":sendername"), message.property("senderName", QString())); + query.exec(); + }); +} + +AsyncResult SqliteHistory::read(const ContactInfo &info, const QDateTime &from, const QDateTime &to, int max_num) +{ + AsyncResultHandler handler; + + m_worker->runJob([info, from, to, max_num, handler] () { + QSqlQuery query; + + QString queryString = "SELECT time, incoming, message, html, type, sendername FROM qutim_history WHERE protocol = :protocol " + "AND account = :account " + "AND contact = :contact "; + if(from.isValid()) + queryString += "AND time >= :time_from "; + + if(to.isValid()) + queryString += "AND time <= :time_to "; + + queryString += "ORDER BY time DESC "; + + if(max_num != -1) + queryString += "LIMIT :max"; + query.prepare(queryString); + + if(from.isValid()) + query.bindValue(QStringLiteral(":time_from"), from.toTime_t()); + if(to.isValid()) + query.bindValue(QStringLiteral(":time_to"), to.toTime_t()); + + qDebug() << "Trying to read messages"; + + query.bindValue(QStringLiteral(":account"), info.account); + query.bindValue(QStringLiteral(":protocol"), info.protocol); + query.bindValue(QStringLiteral(":contact"), info.contact); + + query.bindValue(QStringLiteral(":max"), max_num); + + query.exec(); + + auto error = query.lastError(); + if (error.isValid()) { + qDebug() << "Error type: " << error.type(); + qDebug() << "Database: " << error.databaseText(); + qDebug() << "Driver: " << error.driverText(); + } + + MessageList items; + + while(query.next()) { + Message msg; + msg.setTime(QDateTime::fromTime_t(query.value(0).toUInt())); + msg.setIncoming(query.value(1).toBool()); + msg.setText(query.value(2).toString()); + msg.setHtml(query.value(3).toString()); + + if(!query.value(5).isNull()) + msg.setProperty("senderName", query.value(5).toString()); + + SqliteWorker::MessageTypes type = static_cast(query.value(4).toInt()); + + if(type.testFlag(SqliteWorker::Topic)) + msg.setProperty("topic", true); + + if(type.testFlag(SqliteWorker::Service)) + msg.setProperty("service", true); + + items.append(msg); + } + + std::stable_sort(items.begin(), items.end(), [](const Message &left, const Message &right) { + return left.time() < right.time(); + }); + + handler.handle(items); + }); + + return handler.result(); +} + +AsyncResult> SqliteHistory::accounts() +{ + AsyncResultHandler> handler; + + m_worker->runJob([handler] () { + QVector result; + + QSqlQuery query; + query.prepare(QStringLiteral("SELECT DISTINCT account, protocol FROM qutim_history")); + query.exec(); + qDebug() << "accounts()"; + + while(query.next()) { + AccountInfo info; + info.account = query.value(0).toString(); + info.protocol = query.value(1).toString(); + qDebug() << "AccountInfo" << info.protocol << info.account; + result << info; + } + + handler.handle(result); + }); + + return handler.result(); +} + +AsyncResult> SqliteHistory::contacts(const AccountInfo &account) +{ + AsyncResultHandler> handler; + + m_worker->runJob([handler, account] () { + QVector result; + + qDebug() << "contacts()"; + QSqlQuery query; + query.prepare(QStringLiteral("SELECT DISTINCT contact FROM qutim_history WHERE account = :account AND protocol = :protocol")); + query.bindValue(QStringLiteral(":account"), account.account); + query.bindValue(QStringLiteral(":protocol"), account.protocol); + query.exec(); + + auto error = query.lastError(); + if (error.isValid()) { + qDebug() << "Error type: " << error.type(); + qDebug() << "Database: " << error.databaseText(); + qDebug() << "Driver: " << error.driverText(); + } + + + while(query.next()) { + ContactInfo info; + info.account = account.account; + info.protocol = account.protocol; + info.contact = query.value(0).toString(); + + qDebug() << info.contact; + result << info; + } + + handler.handle(result); + }); + + return handler.result(); +} + +AsyncResult> SqliteHistory::months(const ContactInfo &contact, const QString &needle) +{ + AsyncResultHandler> handler; + + m_worker->runJob([handler, contact, needle] () { + QList result; + + QString queryString = QStringLiteral("SELECT DISTINCT year, month FROM qutim_history WHERE account = :account " + "AND protocol = :protocol " + "AND contact = :contact "); + + if(!needle.isEmpty()) + queryString += QStringLiteral("AND message LIKE :needle ESCAPE '@'"); + QSqlQuery query; + query.prepare(queryString); + query.bindValue(QStringLiteral(":account"), contact.account); + query.bindValue(QStringLiteral(":protocol"), contact.protocol); + query.bindValue(QStringLiteral(":contact"), contact.contact); + if(!needle.isEmpty()) + query.bindValue(QStringLiteral(":needle"), QLatin1Char('%') + SqliteWorker::escapeSqliteLike(needle) + QLatin1Char('%')); + query.exec(); + qDebug() << "months()"; + + while(query.next()) { + int year = query.value(0).toInt(); + int month = query.value(1).toInt(); + + qDebug() << QDate(year, month, 1); + result << QDate(year, month, 1); + } + + std::sort(result.begin(), result.end()); + + handler.handle(result); + }); + + return handler.result(); +} + +AsyncResult> SqliteHistory::dates(const ContactInfo &contact, const QDate &month, const QString &needle) +{ + AsyncResultHandler> handler; + + m_worker->runJob([handler, contact, month, needle] () { + QList result; + + QString queryString = QStringLiteral("SELECT DISTINCT day FROM qutim_history WHERE account = :account " + "AND protocol = :protocol " + "AND contact = :contact " + "AND year = :year " + "AND month = :month "); + if(!needle.isEmpty()) + queryString += QStringLiteral("AND message LIKE :needle ESCAPE '@'"); + + QSqlQuery query; + query.prepare(queryString); + query.bindValue(QStringLiteral(":account"), contact.account); + query.bindValue(QStringLiteral(":protocol"), contact.protocol); + query.bindValue(QStringLiteral(":contact"), contact.contact); + query.bindValue(QStringLiteral(":year"), month.year()); + query.bindValue(QStringLiteral(":month"), month.month()); + if(!needle.isEmpty()) + query.bindValue(QStringLiteral(":needle"), QLatin1Char('%') + SqliteWorker::escapeSqliteLike(needle) + QLatin1Char('%')); + query.exec(); + + while(query.next()) { + result << QDate(month.year(), month.month(), query.value(0).toInt()); + } + + std::sort(result.begin(), result.end()); + + handler.handle(result); + }); + + return handler.result(); +} + +void SqliteHistory::errorHandler(const QString &error) +{ + qDebug() << error; +} + +void SqliteWorker::prepareDb() +{ + QString dbScheme = QStringLiteral("CREATE TABLE IF NOT EXISTS qutim_history (" + "`id` INTEGER PRIMARY KEY AUTOINCREMENT, " + "`account` TEXT NOT NULL, " + "`protocol` TEXT NOT NULL, " + "`contact` TEXT NOT NULL, " + "`year` INTEGER(2) NOT NULL, " + "`month` INTEGER(1) NOT NULL, " + "`day` INTEGER(1) NOT NULL, " + "`time` TIMESTAMP NOT NULL, " + "`incoming` INTEGER(1) NOT NULL, " + "`message` TEXT NOT NULL, " + "`html` TEXT NOT NULL, " + "`type` INTEGER(1) NOT NULL, " + "`sendername` TEXT DEFAULT NULL" + ");"); + + QSqlQuery res = m_db.exec(dbScheme); + auto error = res.lastError(); + if(error.isValid()) + qDebug() << "Error during db init:" << error; + + QString dbMigrations = QStringLiteral("CREATE TABLE IF NOT EXISTS qutim_history_version (" + "`key` VARCHAR(32) NOT NULL, " + "`value` INTEGER(1) NOT NULL, " + "primary key (`key`)" + ");"); + QSqlQuery migrationsResult = m_db.exec(dbMigrations); + auto migrationsError = migrationsResult.lastError(); + if(migrationsError.isValid()) + qDebug() << "Error during creation of qutim_history_version:" << error; + + qDebug() << "Checking version of database..."; + + QSqlQuery query; + query.prepare(QStringLiteral("SELECT value FROM qutim_history_version WHERE key = :key LIMIT 1")); + query.bindValue(QStringLiteral(":key"), QStringLiteral("sqlitehistory")); + query.exec(); + + if(query.first()) { + int version = query.value(0).toInt(); + if(version == currentVersion()) { + qDebug() << "No migration needed"; + return; + } else if(version > currentVersion()) { + qDebug() << "Current version of sqlitehistory plugin is" << currentVersion(); + qDebug() << "But qutim.sqlite version = " << version; + qFatal("qutIM sqlite database is older than plugin. Cannot proceed. Please upgrade qutIM to last version"); + } else if(version < currentVersion()) { + qDebug() << "Database older than plugin, executing migrations"; + + for(int i = version + 1; i < currentVersion(); ++i) + makeMigration(i); + } + } else { + qDebug() << "First run, inserting current version"; + QSqlQuery query; + + query.prepare(QStringLiteral("INSERT INTO qutim_history_version (key, value) " + "VALUES (:key, :value)")); + query.bindValue(QStringLiteral(":key"), QStringLiteral("sqlitehistory")); + query.bindValue(QStringLiteral(":value"), currentVersion()); + query.exec(); + + qDebug() << "No migration needed"; + } +} + +void SqliteWorker::exec() +{ + forever { + m_queueLock.lock(); + if(m_queue.isEmpty()) { + m_runningLock.lock(); + m_isRunning = false; + m_runningLock.unlock(); + + m_queueLock.unlock(); + break; + } + + auto f = m_queue.dequeue(); + m_queueLock.unlock(); + + f(); + } +} + +void SqliteWorker::runJob(std::function job) +{ + m_queueLock.lock(); + m_queue.enqueue(std::move(job)); + m_queueLock.unlock(); + + m_runningLock.lock(); + if(!m_isRunning) { + m_isRunning = true; + m_runningLock.unlock(); + QTimer::singleShot(0, this, SLOT(exec())); + } else + m_runningLock.unlock(); +} + +void SqliteWorker::shutdown() +{ + emit finished(); +} + +/** + * Escapes string to use in LIKE query. You need to add ESCAPE '@' to end of your query + */ +QString SqliteWorker::escapeSqliteLike(const QString &str) +{ + QString escaped = str; + escaped.replace(QLatin1Char('@'), QStringLiteral("@@")); + escaped.replace(QLatin1Char('_'), QStringLiteral("@_")); + escaped.replace(QLatin1Char('%'), QStringLiteral("@%")); + + return escaped; +} + +void SqliteWorker::process() +{ + m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); + QDir history_dir = SystemInfo::getDir(SystemInfo::HistoryDir); + QString pathToHistory = history_dir.absolutePath() + QDir::separator() + "qutim.sqlite"; + qDebug() << "Trying to open database" << pathToHistory; + m_db.setDatabaseName(pathToHistory); + bool openSuccess = m_db.open(); + + if(!openSuccess) { + qDebug() << m_db.lastError(); + qFatal("Cannot open sqlite database"); + } else + qDebug() << "Database opened!"; + + prepareDb(); + + m_runningLock.lock(); + m_isRunning = true; + m_runningLock.unlock(); + exec(); +} + +void SqliteWorker::makeMigration(int version) +{ + qDebug() << "Executing migration to version" << version; + m_db.transaction(); + QSqlQuery query; + + switch(version) { + case 0: + query.prepare(QStringLiteral("ALTER TABLE qutim_history ADD COLUMN `sendername` TEXT DEFAULT NULL")); + break; + default: + qFatal("Unhandled migration to version %i! Seems like a bug", version); + break; + } + + query.exec(); + + auto error = query.lastError(); + if(error.isValid()) { + qDebug() << "Error during migration from" << version - 1 << "to" << version; + qDebug() << "Error type: " << error.type(); + qDebug() << "Database: " << error.databaseText(); + qDebug() << "Driver: " << error.driverText(); + + m_db.rollback(); + qFatal("Exiting..."); + } else { + qDebug() << "Successful migration to version" << version; + QSqlQuery q; + q.prepare(QStringLiteral("UPDATE qutim_history_version SET value = :value WHERE key = :key")); + q.bindValue(QStringLiteral(":key"), QStringLiteral("sqlitehistory")); + q.bindValue(QStringLiteral(":value"), version); + q.exec(); + + m_db.commit(); + } +} + +} + diff --git a/src/plugins/generic/sqlitehistory/sqlitehistory.h b/src/plugins/generic/sqlitehistory/sqlitehistory.h new file mode 100644 index 000000000..06983c5cb --- /dev/null +++ b/src/plugins/generic/sqlitehistory/sqlitehistory.h @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** qutIM - instant messenger +** +** Copyright © 2015 Nicolay Izoderov +** +***************************************************************************** +** +** $QUTIM_BEGIN_LICENSE$ +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +** See the GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see http://www.gnu.org/licenses/. +** $QUTIM_END_LICENSE$ +** +****************************************************************************/ + +#ifndef SQLITEHISTORY_H +#define SQLITEHISTORY_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace qutim_sdk_0_3; + +namespace Core +{ +class SqliteWorker; + +class SqliteHistory : public History +{ + Q_OBJECT +public: + SqliteHistory(); + virtual ~SqliteHistory(); + + void store(const Message &message) override; + AsyncResult read(const ContactInfo &info, const QDateTime &from, const QDateTime &to, int max_num) override; + AsyncResult> accounts() override; + AsyncResult> contacts(const AccountInfo &account) override; + AsyncResult> months(const ContactInfo &contact, const QString &needle) override; + AsyncResult> dates(const ContactInfo &contact, const QDate &month, const QString &needle) override; + +public slots: + void errorHandler(const QString & error); + +private: + QThread* m_thread; + SqliteWorker* m_worker; +}; + +class SqliteWorker : public QObject +{ + Q_OBJECT +public: + + enum MessageType { + Message = 0, + Topic = 1, + Service = 2 + }; + Q_DECLARE_FLAGS(MessageTypes, MessageType) + + static QString escapeSqliteLike(const QString &str); + void runJob(std::function job); + void shutdown(); +public slots: + void process(); +signals: + void finished(); + void error(const QString &error); + +private slots: + void exec(); +private: + inline int currentVersion() { return 1; } + void makeMigration(int version); + QQueue> m_queue; + QMutex m_queueLock; + QMutex m_runningLock; + QSqlDatabase m_db; + void prepareDb(); + bool m_isRunning = false; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(SqliteWorker::MessageTypes) + +} + +#endif // SQLITEHISTORY_H + diff --git a/src/plugins/generic/sqlitehistory/sqlitehistory.plugin.json b/src/plugins/generic/sqlitehistory/sqlitehistory.plugin.json new file mode 100644 index 000000000..cf5076953 --- /dev/null +++ b/src/plugins/generic/sqlitehistory/sqlitehistory.plugin.json @@ -0,0 +1,7 @@ +{ + "pluginIcon": "", + "pluginName": "Sqlite History", + "pluginDescription": "New qutIM history implementation using sqlite", + "extensionHeader": "sqlitehistory.h", + "extensionClass": "Core::SqliteHistory" +} diff --git a/src/plugins/generic/sqlitehistory/sqlitehistory.qbs b/src/plugins/generic/sqlitehistory/sqlitehistory.qbs new file mode 100644 index 000000000..f268769d8 --- /dev/null +++ b/src/plugins/generic/sqlitehistory/sqlitehistory.qbs @@ -0,0 +1,8 @@ +import "../GenericPlugin.qbs" as GenericPlugin + +GenericPlugin { + sourcePath: '' + Depends { + name: "Qt.sql" + } +}