#include #include #include #include #include #include "global.h" #include "action.h" #include "fetchboardconfigaction.h" #include "fetchforumsaction.h" #include "xmlrpcinterface.h" #include "board.h" const QLatin1String Board::CURRENT_DB_VERSION("testing2"); Board::Board(const QString& forumUrl, QObject *parent) : QObject(parent), _url(forumUrl), _slug(createSlug(forumUrl)), _db(QSqlDatabase::addDatabase("QSQLITE", _slug)), _iface(new XmlRpcInterface(QUrl(_url), this)) { _db.setDatabaseName(QDir::toNativeSeparators(getDbPathFor(_slug))); qDebug() << "Opening database file" << _db.databaseName() << "for" << _url; if (!_db.open()) { qWarning() << "Could not open database file" << _db.databaseName() << ":" << _db.lastError().text(); _db.setDatabaseName(QDir::toNativeSeparators(getTempDbPathFor(_slug))); if (!_db.open()) { qWarning() << "Could not open temp database file" << _db.databaseName() << ":" << _db.lastError().text(); return; // Give up } } if (!checkCompatibleDb()) { qDebug() << "Database version incompatible, reinitializing"; eraseDb(); initializeDb(); } fetchConfigIfOutdated(); fetchForumsIfOutdated(); initializeBbCode(); // TODO This might depend on board config } Board::~Board() { QSqlDatabase::removeDatabase(_slug); } void Board::enqueueAction(Action *action) { // Let's find if a duplicate action is in there already. qDebug() << "Enqueing" << action; foreach (Action *a, _queue) { if (a->isSupersetOf(action)) { qDebug() << "Action superseded, ignoring"; delete action; return; } } // Otherwise, enqueue the action connect(action, SIGNAL(finished(Action*)), SLOT(handleActionFinished(Action*))); connect(action, SIGNAL(error(Action*,QString)), SLOT(handleActionError(Action*,QString))); _queue.enqueue(action); if (_queue.size() == 1) { // There were no actions queued, so start by executing this one. executeActionFromQueue(); } } QString Board::getConfig(const QString &key) const { // Try config cache first QHash::const_iterator i = _config.find(key); if (i != _config.end()) { // Cache hit return i.value(); } // Try database QSqlQuery query(_db); query.prepare("SELECT key, value FROM config WHERE key = :key"); query.bindValue(":key", key); if (!query.exec()) { qWarning() << "Could not get configuration key:" << key; return QString(); } if (query.next()) { QString value = query.value(1).toString(); _config[key] = value; // Store in cache return value; } return QString(); } void Board::setConfig(const QString &key, const QString &value) { // Try config cache first QHash::const_iterator i = _config.find(key); if (i != _config.end()) { QString old_value = i.value(); if (old_value == value) { // It's in the cache, and it's the same value: don't change. return; } } // Update value in DB QSqlQuery query(_db); query.prepare("INSERT OR REPLACE INTO config (key, value) VALUES (:key, :value)"); query.bindValue(":key", key); query.bindValue(":value", value); if (!query.exec()) { qWarning() << "Could not set configuration key" << key << ":" << query.lastError().text(); } _config.insert(key, value); notifyConfigChanged(key); } QString Board::removeHtml(QString text) const { static const QRegExp regexp("<[a-zA-Z\\/][^>]*>"); text.replace(regexp, ""); return text; } QString Board::removeBbcode(QString text) const { static const QRegExp regexp("\\[[a-zA-Z\\/][^]]*\\]"); text.replace(regexp, ""); return text; } QString Board::bbcodeToRichText(QString text) const { typedef QPair Pair; // Workaround for ',' in Q_FOREACH foreach (const Pair& pair, _bbcodes) { text.replace(pair.first, pair.second); } return text; } QString Board::renderHumanDate(const QDateTime &dateTime) { QDate date = dateTime.toLocalTime().date(); QDate today = QDate::currentDate(); if (date == today) { return tr("Today"); } else if (date.daysTo(today) == 1) { return tr("Yesterday"); } else if (date.daysTo(today) < 5) { return QDate::longDayName(date.dayOfWeek(), QDate::StandaloneFormat); } else { return date.toString(Qt::DefaultLocaleShortDate); } } QString Board::renderHumanTime(const QDateTime &dateTime) { return dateTime.toLocalTime().time().toString(Qt::DefaultLocaleShortDate); } void Board::notifyConfigChanged(const QString& key) { if (!key.isEmpty()) { _config.remove(key); } else { // Must assume all keys were changed _config.clear(); } emit configChanged(key); } void Board::notifyForumsChanged() { emit forumsChanged(); } void Board::notifyForumTopicsChanged(int forumId, int start, int end) { qDebug() << "ForumTopics Changed" << forumId << start << end; emit forumTopicsChanged(forumId, start, end); } void Board::notifyTopicPostsChanged(int topicId, int start, int end) { qDebug() << "TopicPosts Changed" << topicId << start << end; emit topicPostsChanged(topicId, start, end); } QString Board::createSlug(const QString &forumUrl) { static const QRegExp regexp("[^a-z0-9]+"); QString url = forumUrl.toLower(); url.replace(regexp, "_"); return url; } QString Board::getDbPathFor(const QString &slug) { return board_manager->getCachePath() + "/" + slug + ".sqlite"; } QString Board::getTempDbPathFor(const QString& slug) { return QDir::tempPath() + "/" + slug + ".sqlite"; } bool Board::checkCompatibleDb() { QString version = getConfig("tapasboard_db_version"); return version == CURRENT_DB_VERSION; } bool Board::initializeDb() { QSqlQuery q(_db); if (!q.exec("CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)")) { qWarning() << "Could not create config table:" << q.lastError().text(); return false; } if (!q.exec("CREATE TABLE IF NOT EXISTS forums (forum_id INTEGER PRIMARY KEY, forum_name TEXT, description TEXT, parent_id INT, logo_url TEXT, new_post BOOL, is_protected BOOL, is_subscribed BOOL, can_subscribe BOOL, url TEXT, sub_only BOOL, sort_index INT)")) { qWarning() << "Could not create forums table:" << q.lastError().text(); return false; } if (!q.exec("CREATE INDEX IF NOT EXISTS forums_parent ON forums (parent_id)")) { qWarning() << "Could not create forums_parent index:" << q.lastError().text(); return false; } if (!q.exec("CREATE UNIQUE INDEX IF NOT EXISTS forums_order ON forums (sort_index ASC)")) { qWarning() << "Could not create forums_order index:" << q.lastError().text(); return false; } if (!q.exec("CREATE TABLE IF NOT EXISTS topics (forum_id INTEGER, topic_id INTEGER PRIMARY KEY, topic_title TEXT, topic_author_id INTEGER, topic_author_name TEXT, is_subscribed BOOL, is_closed BOOL, icon_url TEXT, last_reply_time TEXT, reply_number INT, new_post BOOL, last_update_time TEXT)")) { qWarning() << "Could not create topics table:" << q.lastError().text(); return false; } if (!q.exec("CREATE INDEX IF NOT EXISTS topics_forum ON topics (forum_id)")) { qWarning() << "Could not create topics_forum index:" << q.lastError().text(); return false; } if (!q.exec("CREATE INDEX IF NOT EXISTS topics_time ON topics (last_reply_time)")) { qWarning() << "Could not create topics_time index:" << q.lastError().text(); return false; } if (!q.exec("CREATE TABLE IF NOT EXISTS posts (forum_id INTEGER, topic_id INTEGER, post_id INTEGER PRIMARY KEY, post_title TEXT, post_content TEXT, post_author_id INTEGER, post_author_name TEXT, can_edit BOOL, icon_url TEXT, post_time TEXT, last_update_time TEXT)")) { qWarning() << "Could not create posts table:" << q.lastError().text(); return false; } if (!q.exec("CREATE INDEX IF NOT EXISTS posts_topic ON posts (topic_id)")) { qWarning() << "Could not create posts_topic index:" << q.lastError().text(); return false; } if (!q.exec("CREATE INDEX IF NOT EXISTS posts_time ON posts (post_time)")) { qWarning() << "Could not create posts_time index:" << q.lastError().text(); return false; } return true; } bool Board::eraseDb() { QSqlQuery q(_db); if (!q.exec("DROP TABLE IF EXISTS config")) { qWarning() << "Could not drop config table:" << q.lastError().text(); return false; } if (!q.exec("DROP TABLE IF EXISTS forums")) { qWarning() << "Could not drop forums table:" << q.lastError().text(); return false; } if (!q.exec("DROP TABLE IF EXISTS topics")) { qWarning() << "Could not drop topics table:" << q.lastError().text(); return false; } if (!q.exec("DROP TABLE IF EXISTS posts")) { qWarning() << "Could not drop posts table:" << q.lastError().text(); return false; } if (!q.exec("VACUUM")) { qWarning() << "Could not vacuum database:" << q.lastError().text(); return false; } return true; } bool Board::cleanDb() { QSqlQuery q(_db); // TODO: Delete old posts from cache if (!q.exec("VACUUM")) { qWarning() << "Could not vacuum database:" << q.lastError().text(); return false; } return true; } bool Board::removeFromActionQueue(Action *action) { if (_queue.isEmpty()) return false; Action *head = _queue.head(); if (_queue.removeOne(action)) { if (!_queue.isEmpty() && head != _queue.head()) { // The head action was removed; advance the queue. executeActionFromQueue(); } action->deleteLater(); return true; } return false; } void Board::executeActionFromQueue() { if (!_queue.empty()) { Action *head = _queue.head(); head->execute(); } } void Board::initializeBbCode() { _bbcodes.clear(); _bbcodes << qMakePair(QRegExp("\\[(/?[bius])\\]", Qt::CaseInsensitive), QString("<\\1>")); _bbcodes << qMakePair(QRegExp("\\[(/?)quote\\]", Qt::CaseInsensitive), QString("<\\1blockquote>")); _bbcodes << qMakePair(QRegExp("\\[url\\]([^[]*)\\[/url\\]", Qt::CaseInsensitive), QString("\\1")); _bbcodes << qMakePair(QRegExp("\\[url=([^]]*)\\]", Qt::CaseInsensitive), QString("")); _bbcodes << qMakePair(QRegExp("\\[/url\\]", Qt::CaseInsensitive), QString("")); _bbcodes << qMakePair(QRegExp("\\[img\\]([^[]*)\\[/img\\]", Qt::CaseInsensitive), QString("")); _bbcodes << qMakePair(QRegExp("\\[hr\\]", Qt::CaseInsensitive), QString("
")); _bbcodes << qMakePair(QRegExp("\n"), QString("
")); } void Board::fetchConfigIfOutdated() { if (_iface->isAccessible()) { // Only fetch if network is accessible and data is >48h old. QDateTime last_fetch = QDateTime::fromString( getConfig("last_config_fetch"), Qt::ISODate); if (!last_fetch.isValid() || last_fetch.daysTo(QDateTime::currentDateTimeUtc()) >= BOARD_CONFIG_TTL) { enqueueAction(new FetchBoardConfigAction(this)); } } } void Board::fetchForumsIfOutdated() { if (_iface->isAccessible()) { // Only fetch if network is accessible and data is >48h old. QDateTime last_fetch = QDateTime::fromString( getConfig("last_forums_fetch"), Qt::ISODate); if (!last_fetch.isValid() || last_fetch.daysTo(QDateTime::currentDateTimeUtc()) >= BOARD_LIST_TTL) { enqueueAction(new FetchForumsAction(this)); } } } void Board::handleActionFinished(Action *action) { qDebug() << action << "finished"; bool ok = removeFromActionQueue(action); if (!ok) { qWarning() << "Finished action not in queue"; } } void Board::handleActionError(Action *action, const QString& message) { qWarning() << "Action failed:" << message; removeFromActionQueue(action); }