From 0ee8f90d40f5b3ddb177286c9066a4e59748c681 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 15 Dec 2013 14:48:58 -0600 Subject: [PATCH] Added a system to load news from MultiMC.org's RSS Currently it doesn't show it anywhere in the UI. That's next. --- CMakeLists.txt | 13 +++- MultiMC.cpp | 5 ++ MultiMC.h | 7 ++ config.h.in | 4 ++ gui/MainWindow.cpp | 4 ++ logic/news/NewsChecker.cpp | 130 +++++++++++++++++++++++++++++++++++++ logic/news/NewsChecker.h | 105 ++++++++++++++++++++++++++++++ logic/news/NewsEntry.cpp | 77 ++++++++++++++++++++++ logic/news/NewsEntry.h | 65 +++++++++++++++++++ 9 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 logic/news/NewsChecker.cpp create mode 100644 logic/news/NewsChecker.h create mode 100644 logic/news/NewsEntry.cpp create mode 100644 logic/news/NewsEntry.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dadbad4..7880d0d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,6 +110,11 @@ IF(${BIGENDIAN}) ENDIF(${BIGENDIAN}) +######## Set URLs ######## + +SET(MultiMC_NEWS_RSS_URL "http://multimc.org/rss.xml" CACHE STRING "URL to fetch MultiMC's news RSS feed from.") + + ######## Set version numbers ######## SET(MultiMC_VERSION_MAJOR 1) SET(MultiMC_VERSION_MINOR 0) @@ -325,6 +330,12 @@ logic/updater/UpdateChecker.cpp logic/updater/DownloadUpdateTask.h logic/updater/DownloadUpdateTask.cpp +# News System +logic/news/NewsChecker.h +logic/news/NewsChecker.cpp +logic/news/NewsEntry.h +logic/news/NewsEntry.cpp + # legacy instances logic/LegacyInstance.h logic/LegacyInstance.cpp @@ -477,7 +488,7 @@ ADD_EXECUTABLE(MultiMC MACOSX_BUNDLE WIN32 # Link TARGET_LINK_LIBRARIES(MultiMC xz-embedded unpack200 quazip libUtil libSettings libGroupView ${MultiMC_LINK_ADDITIONAL_LIBS}) -QT5_USE_MODULES(MultiMC Core Widgets Network Xml ${MultiMC_QT_ADDITIONAL_MODULES}) +QT5_USE_MODULES(MultiMC Core Widgets Network Xml WebKit ${MultiMC_QT_ADDITIONAL_MODULES}) ADD_DEPENDENCIES(MultiMC MultiMCLauncher JavaCheck) ################################ INSTALLATION AND PACKAGING ################################ diff --git a/MultiMC.cpp b/MultiMC.cpp index 2c9e74dd..52bf4cb9 100644 --- a/MultiMC.cpp +++ b/MultiMC.cpp @@ -18,6 +18,8 @@ #include "logic/lists/MinecraftVersionList.h" #include "logic/lists/ForgeVersionList.h" +#include "logic/news/NewsChecker.h" + #include "logic/InstanceLauncher.h" #include "logic/net/HttpMetaCache.h" @@ -147,6 +149,9 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv), // initialize the updater m_updateChecker.reset(new UpdateChecker()); + // initialize the news checker + m_newsChecker.reset(new NewsChecker(NEWS_RSS_URL)); + // and instances auto InstDirSetting = m_settings->getSetting("InstanceDir"); m_instances.reset(new InstanceList(InstDirSetting->get().toString(), this)); diff --git a/MultiMC.h b/MultiMC.h index 22cea029..fe6e0e76 100644 --- a/MultiMC.h +++ b/MultiMC.h @@ -18,6 +18,7 @@ class QNetworkAccessManager; class ForgeVersionList; class JavaVersionList; class UpdateChecker; +class NewsChecker; #if defined(MMC) #undef MMC @@ -90,6 +91,11 @@ public: return m_updateChecker; } + std::shared_ptr newsChecker() + { + return m_newsChecker; + } + std::shared_ptr lwjgllist(); std::shared_ptr forgelist(); @@ -129,6 +135,7 @@ private: std::shared_ptr m_settings; std::shared_ptr m_instances; std::shared_ptr m_updateChecker; + std::shared_ptr m_newsChecker; std::shared_ptr m_accounts; std::shared_ptr m_icons; std::shared_ptr m_qnam; diff --git a/config.h.in b/config.h.in index b58dc322..aa604056 100644 --- a/config.h.in +++ b/config.h.in @@ -16,3 +16,7 @@ // This is printed on start to standard output #define VERSION_STR "@MultiMC_VERSION_STRING@" +// This is used to fetch the news RSS feed. +// It defaults in CMakeLists.txt to "http://multimc.org/rss.xml" +#define NEWS_RSS_URL "@MultiMC_NEWS_RSS_URL@" + diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp index f6ef6bad..748728d6 100644 --- a/gui/MainWindow.cpp +++ b/gui/MainWindow.cpp @@ -76,6 +76,8 @@ #include "logic/updater/DownloadUpdateTask.h" +#include "logic/news/NewsChecker.h" + #include "logic/net/URLConstants.h" #include "logic/BaseInstance.h" @@ -243,6 +245,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi MMC->lwjgllist()->loadList(); } + MMC->newsChecker()->reloadNews(); + // set up the updater object. auto updater = MMC->updateChecker(); QObject::connect(updater.get(), &UpdateChecker::updateAvailable, this, &MainWindow::updateAvailable); diff --git a/logic/news/NewsChecker.cpp b/logic/news/NewsChecker.cpp new file mode 100644 index 00000000..08d4ef23 --- /dev/null +++ b/logic/news/NewsChecker.cpp @@ -0,0 +1,130 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "NewsChecker.h" + +#include +#include + +#include + +NewsChecker::NewsChecker(const QString& feedUrl) +{ + m_feedUrl = feedUrl; +} + +void NewsChecker::reloadNews() +{ + // Start a netjob to download the RSS feed and call rssDownloadFinished() when it's done. + if (isLoadingNews()) + { + QLOG_INFO() << "Ignored request to reload news. Currently reloading already."; + return; + } + + QLOG_INFO() << "Reloading news."; + + NetJob* job = new NetJob("News RSS Feed"); + job->addNetAction(ByteArrayDownload::make(m_feedUrl)); + QObject::connect(job, &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); + QObject::connect(job, &NetJob::failed, this, &NewsChecker::rssDownloadFailed); + m_newsNetJob.reset(job); + job->start(); +} + +void NewsChecker::rssDownloadFinished() +{ + // Parse the XML file and process the RSS feed entries. + QLOG_DEBUG() << "Finished loading RSS feed."; + + QByteArray data; + { + ByteArrayDownloadPtr dl = std::dynamic_pointer_cast(m_newsNetJob->first()); + data = dl->m_data; + m_newsNetJob.reset(); + } + + QDomDocument doc; + { + // Stuff to store error info in. + QString errorMsg = "Unknown error."; + int errorLine = -1; + int errorCol = -1; + + // Parse the XML. + if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) + { + QString fullErrorMsg = QString("Error parsing RSS feed XML. %s at %d:%d.").arg(errorMsg, errorLine, errorCol); + fail(fullErrorMsg); + return; + } + } + + // If the parsing succeeded, read it. + QDomNodeList items = doc.elementsByTagName("item"); + m_newsEntries.clear(); + for (int i = 0; i < items.length(); i++) + { + QDomElement element = items.at(i).toElement(); + NewsEntryPtr entry; + entry.reset(new NewsEntry()); + QString errorMsg = "An unknown error occurred."; + if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg)) + { + QLOG_DEBUG() << "Loaded news entry" << entry->title; + m_newsEntries.append(entry); + } + else + { + QLOG_WARN() << "Failed to load news entry at index" << i << ":" << errorMsg; + } + } + + succeed(); +} + +void NewsChecker::rssDownloadFailed() +{ + // Set an error message and fail. + fail("Failed to load news RSS feed."); +} + + +bool NewsChecker::isLoadingNews() const +{ + return m_newsNetJob.get() != nullptr; +} + +QString NewsChecker::getLastLoadErrorMsg() const +{ + return m_lastLoadError; +} + +void NewsChecker::succeed() +{ + m_lastLoadError = ""; + QLOG_DEBUG() << "News loading succeeded."; + m_newsNetJob.reset(); + emit newsLoaded(); +} + +void NewsChecker::fail(const QString& errorMsg) +{ + m_lastLoadError = errorMsg; + QLOG_DEBUG() << "Failed to load news:" << errorMsg; + m_newsNetJob.reset(); + emit newsLoadingFailed(errorMsg); +} + diff --git a/logic/news/NewsChecker.h b/logic/news/NewsChecker.h new file mode 100644 index 00000000..6d1fb935 --- /dev/null +++ b/logic/news/NewsChecker.h @@ -0,0 +1,105 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +#include "NewsEntry.h" + +class NewsChecker : public QObject +{ + Q_OBJECT +public: + /*! + * Constructs a news reader to read from the given RSS feed URL. + */ + NewsChecker(const QString& feedUrl); + + /*! + * Returns the error message for the last time the news was loaded. + * Empty string if the last load was successful. + */ + QString getLastLoadErrorMsg() const; + + /*! + * Returns true if the news has been loaded successfully. + */ + bool isNewsLoaded() const; + + /*! + * Returns a list of news entries. + */ + QList getNewsEntries() const; + + /*! + * Reloads the news from the website's RSS feed. + * If the news is already loading, this does nothing. + */ + void Q_SLOT reloadNews(); + +signals: + /*! + * Signal fired after the news has finished loading. + */ + void newsLoaded(); + + /*! + * Signal fired after the news fails to load. + */ + void newsLoadingFailed(QString errorMsg); + +protected slots: + void rssDownloadFinished(); + void rssDownloadFailed(); + +protected: + //! The URL for the RSS feed to fetch. + QString m_feedUrl; + + //! List of news entries. + QList m_newsEntries; + + //! The network job to use to load the news. + NetJobPtr m_newsNetJob; + + //! True if the news is currently loading. If true, reloadNews() will do nothing. + bool isLoadingNews() const; + + //! True if news has been loaded. + bool m_loadedNews; + + /*! + * Gets the error message that was given last time the news was loaded. + * If the last news load succeeded, this will be an empty string. + */ + QString m_lastLoadError; + + + /*! + * Emits newsLoaded() and sets m_lastLoadError to empty string. + */ + void Q_SLOT succeed(); + + /*! + * Emits newsLoadingFailed() and sets m_lastLoadError to the given message. + */ + void Q_SLOT fail(const QString& errorMsg); +}; + diff --git a/logic/news/NewsEntry.cpp b/logic/news/NewsEntry.cpp new file mode 100644 index 00000000..4c940f2e --- /dev/null +++ b/logic/news/NewsEntry.cpp @@ -0,0 +1,77 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "NewsEntry.h" + +#include +#include + +NewsEntry::NewsEntry(QObject* parent) : + QObject(parent) +{ + this->title = tr("Untitled"); + this->content = tr("No content."); + this->link = ""; + this->author = tr("Unknown Author"); + this->pubDate = QDateTime::currentDateTime(); +} + +NewsEntry::NewsEntry(const QString& title, const QString& content, const QString& link, const QString& author, const QDateTime& pubDate, QObject* parent) : + QObject(parent) +{ + this->title = title; + this->content = content; + this->link = link; + this->author = author; + this->pubDate = pubDate; +} + +/*! + * Gets the text content of the given child element as a QVariant. + */ +inline QString childValue(const QDomElement& element, const QString& childName, QString defaultVal="") +{ + QDomNodeList nodes = element.elementsByTagName(childName); + if (nodes.count() > 0) + { + QDomElement element = nodes.at(0).toElement(); + return element.text(); + } + else + { + return defaultVal; + } +} + +bool NewsEntry::fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg) +{ + QString title = childValue(element, "title", tr("Untitled")); + QString content = childValue(element, "description", tr("No content.")); + QString link = childValue(element, "link"); + QString author = childValue(element, "dc:creator", tr("Unknown Author")); + QString pubDateStr = childValue(element, "pubDate"); + + // FIXME: For now, we're just ignoring timezones. We assume that all time zones in the RSS feed are the same. + QString dateFormat("ddd, dd MMM yyyy hh:mm:ss"); + QDateTime pubDate = QDateTime::fromString(pubDateStr, dateFormat); + + entry->title = title; + entry->content = content; + entry->link = link; + entry->author = author; + entry->pubDate = pubDate; + return true; +} + diff --git a/logic/news/NewsEntry.h b/logic/news/NewsEntry.h new file mode 100644 index 00000000..6bfa1adc --- /dev/null +++ b/logic/news/NewsEntry.h @@ -0,0 +1,65 @@ +/* Copyright 2013 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +class NewsEntry : public QObject +{ + Q_OBJECT + +public: + /*! + * Constructs an empty news entry. + */ + explicit NewsEntry(QObject* parent=0); + + /*! + * Constructs a new news entry. + * Note that content may contain HTML. + */ + NewsEntry(const QString& title, const QString& content, const QString& link, const QString& author, const QDateTime& pubDate, QObject* parent=0); + + /*! + * Attempts to load information from the given XML element into the given news entry pointer. + * If this fails, the function will return false and store an error message in the errorMsg pointer. + */ + static bool fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg=0); + + + //! The post title. + QString title; + + //! The post's content. May contain HTML. + QString content; + + //! URL to the post. + QString link; + + //! The post's author. + QString author; + + //! The date and time that this post was published. + QDateTime pubDate; +}; + +typedef std::shared_ptr NewsEntryPtr; +