#include #include #include #include #include #include #include "global.h" #include "action.h" #include "fetchboardconfigaction.h" #include "fetchforumsaction.h" #include "xmlrpcinterface.h" #include "board.h" 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(); } initializeDb(); fetchConfigIfOutdated(); fetchForumsIfOutdated(); } void Board::enqueueAction(Action *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 { 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()) { return query.value(1).toString(); } return QString(); } void Board::setConfig(const QString &key, const QString &value) { 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(); } notifyConfigChanged(); } int Board::rootForumId() const { QSqlQuery query(_db); query.exec("SELECT forum_id FROM forums WHERE parent_id = -1"); if (query.next()) { return query.value(0).toInt(); } else { return -1; } } void Board::notifyConfigChanged() { emit configChanged(); } 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); } QString Board::createSlug(const QString &forumUrl) { static const QRegExp regexp("[^a-z0-9]+"); QString url = forumUrl.toLower(); url.replace(regexp, "_"); return url; } QString Board::getDbDir() { QString path; #ifdef Q_OS_LINUX char * xdg_cache_dir = getenv("XDG_CACHE_HOME"); if (xdg_cache_dir) { path = QString::fromLocal8Bit(xdg_cache_dir) + "/tapasboard"; } else { path = QDir::homePath() + "/.cache/tapasboard"; } if (!QDir().mkpath(path)) { qWarning() << "Failed to create directory for databases:" << path; } #endif return path; } QString Board::getDbPathFor(const QString &slug) { return getDbDir() + "/" + slug + ".sqlite"; } 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 UNIQUE)")) { 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 table:" << 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, 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; } 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::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) { removeFromActionQueue(action); } void Board::handleActionError(Action *action, const QString& message) { qWarning() << "Action failed:" << message; removeFromActionQueue(action); }