QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
qgsnewsfeedparser.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsnewsfeedparser.cpp
3 -------------------
4 begin : July 2019
5 copyright : (C) 2019 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15#include "qgsnewsfeedparser.h"
16#include "qgis.h"
20#include "qgslogger.h"
21#include "qgssettings.h"
22#include "qgsjsonutils.h"
23#include "qgsmessagelog.h"
24#include "qgsapplication.h"
25#include <QDateTime>
26#include <QUrlQuery>
27#include <QFile>
28#include <QDir>
29#include <QRegularExpression>
30
31QgsNewsFeedParser::QgsNewsFeedParser( const QUrl &feedUrl, const QString &authcfg, QObject *parent )
32 : QObject( parent )
33 , mBaseUrl( feedUrl.toString() )
34 , mFeedUrl( feedUrl )
35 , mAuthCfg( authcfg )
36 , mSettingsKey( keyForFeed( mBaseUrl ) )
37{
38 // first thing we do is populate with existing entries
39 readStoredEntries();
40
41 QUrlQuery query( feedUrl );
42
43 const qint64 after = settingsFeedLastFetchTime.value( mSettingsKey );
44 if ( after > 0 )
45 query.addQueryItem( QStringLiteral( "after" ), qgsDoubleToString( after, 0 ) );
46
47 QString feedLanguage = settingsFeedLanguage.value( mSettingsKey );
48 if ( feedLanguage.isEmpty() )
49 {
50 feedLanguage = QgsApplication::settingsLocaleUserLocale.valueWithDefaultOverride( QStringLiteral( "en" ) );
51 }
52 if ( !feedLanguage.isEmpty() && feedLanguage != QLatin1String( "C" ) )
53 query.addQueryItem( QStringLiteral( "lang" ), feedLanguage.mid( 0, 2 ) );
54
55 if ( settingsFeedLatitude.exists( mSettingsKey ) && settingsFeedLongitude.exists( mSettingsKey ) )
56 {
57 const double feedLat = settingsFeedLatitude.value( mSettingsKey );
58 const double feedLong = settingsFeedLongitude.value( mSettingsKey );
59
60 // hack to allow testing using local files
61 if ( feedUrl.isLocalFile() )
62 {
63 query.addQueryItem( QStringLiteral( "lat" ), QString::number( static_cast< int >( feedLat ) ) );
64 query.addQueryItem( QStringLiteral( "lon" ), QString::number( static_cast< int >( feedLong ) ) );
65 }
66 else
67 {
68 query.addQueryItem( QStringLiteral( "lat" ), qgsDoubleToString( feedLat ) );
69 query.addQueryItem( QStringLiteral( "lon" ), qgsDoubleToString( feedLong ) );
70 }
71 }
72
73 // bit of a hack to allow testing using local files
74 if ( feedUrl.isLocalFile() )
75 {
76 if ( !query.toString().isEmpty() )
77 mFeedUrl = QUrl( mFeedUrl.toString() + '_' + query.toString() );
78 }
79 else
80 {
81 mFeedUrl.setQuery( query ); // doesn't work for local file urls
82 }
83}
84
85QList<QgsNewsFeedParser::Entry> QgsNewsFeedParser::entries() const
86{
87 return mEntries;
88}
89
91{
92 Entry dismissed;
93 const int beforeSize = mEntries.size();
94 mEntries.erase( std::remove_if( mEntries.begin(), mEntries.end(),
95 [key, &dismissed]( const Entry & entry )
96 {
97 if ( entry.key == key )
98 {
99 dismissed = entry;
100 return true;
101 }
102 return false;
103 } ), mEntries.end() );
104 if ( beforeSize == mEntries.size() )
105 return; // didn't find matching entry
106
107 QgsSettings().remove( QStringLiteral( "%1/%2" ).arg( mSettingsKey ).arg( key ), QgsSettings::Core );
108
109 // also remove preview image, if it exists
110 if ( !dismissed.imageUrl.isEmpty() )
111 {
112 const QString previewDir = QStringLiteral( "%1/previewImages" ).arg( QgsApplication::qgisSettingsDirPath() );
113 const QString imagePath = QStringLiteral( "%1/%2.png" ).arg( previewDir ).arg( key );
114 if ( QFile::exists( imagePath ) )
115 {
116 QFile::remove( imagePath );
117 }
118 }
119
120 if ( !mBlockSignals )
121 emit entryDismissed( dismissed );
122}
123
125{
126 const QList< QgsNewsFeedParser::Entry > entries = mEntries;
127 for ( const Entry &entry : entries )
128 {
129 dismissEntry( entry.key );
130 }
131}
132
134{
135 return mAuthCfg;
136}
137
139{
140 QNetworkRequest req( mFeedUrl );
141 QgsSetRequestInitiatorClass( req, QStringLiteral( "QgsNewsFeedParser" ) );
142
143 mFetchStartTime = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();
144
145 // allow canceling the news fetching without prompts -- it's not crucial if this gets finished or not
147 task->setDescription( tr( "Fetching News Feed" ) );
148 connect( task, &QgsNetworkContentFetcherTask::fetched, this, [this, task]
149 {
150 QNetworkReply *reply = task->reply();
151 if ( !reply )
152 {
153 // canceled
154 return;
155 }
156
157 if ( reply->error() != QNetworkReply::NoError )
158 {
159 QgsMessageLog::logMessage( tr( "News feed request failed [error: %1]" ).arg( reply->errorString() ) );
160 return;
161 }
162
163 // queue up the handling
164 QMetaObject::invokeMethod( this, "onFetch", Qt::QueuedConnection, Q_ARG( QString, task->contentAsString() ) );
165 } );
166
168}
169
170void QgsNewsFeedParser::onFetch( const QString &content )
171{
172 settingsFeedLastFetchTime.setValue( mFetchStartTime, mSettingsKey );
173
174 const QVariant json = QgsJsonUtils::parseJson( content );
175
176 const QVariantList entries = json.toList();
177 QList< QgsNewsFeedParser::Entry > newEntries;
178 newEntries.reserve( entries.size() );
179 for ( const QVariant &e : entries )
180 {
181 Entry newEntry;
182 const QVariantMap entryMap = e.toMap();
183 newEntry.key = entryMap.value( QStringLiteral( "pk" ) ).toInt();
184 newEntry.title = entryMap.value( QStringLiteral( "title" ) ).toString();
185 newEntry.imageUrl = entryMap.value( QStringLiteral( "image" ) ).toString();
186 newEntry.content = entryMap.value( QStringLiteral( "content" ) ).toString();
187 newEntry.link = entryMap.value( QStringLiteral( "url" ) ).toString();
188 newEntry.sticky = entryMap.value( QStringLiteral( "sticky" ) ).toBool();
189 bool ok = false;
190 const uint expiry = entryMap.value( QStringLiteral( "publish_to" ) ).toUInt( &ok );
191 if ( ok )
192 newEntry.expiry.setSecsSinceEpoch( expiry );
193 newEntries.append( newEntry );
194
195 if ( !newEntry.imageUrl.isEmpty() )
196 fetchImageForEntry( newEntry );
197
198 mEntries.append( newEntry );
199 storeEntryInSettings( newEntry );
200 emit entryAdded( newEntry );
201 }
202
203 emit fetched( newEntries );
204}
205
206void QgsNewsFeedParser::readStoredEntries()
207{
208 QgsSettings settings;
209
210 settings.beginGroup( mSettingsKey, QgsSettings::Core );
211 QStringList existing = settings.childGroups();
212 std::sort( existing.begin(), existing.end(), []( const QString & a, const QString & b )
213 {
214 return a.toInt() < b.toInt();
215 } );
216 mEntries.reserve( existing.size() );
217 for ( const QString &entry : existing )
218 {
219 const Entry e = readEntryFromSettings( entry.toInt() );
220 if ( !e.expiry.isValid() || e.expiry > QDateTime::currentDateTime() )
221 mEntries.append( e );
222 else
223 {
224 // expired entry, prune it
225 mBlockSignals = true;
226 dismissEntry( e.key );
227 mBlockSignals = false;
228 }
229 }
230}
231
232QgsNewsFeedParser::Entry QgsNewsFeedParser::readEntryFromSettings( const int key )
233{
234 const QString baseSettingsKey = QStringLiteral( "%1/%2" ).arg( mSettingsKey ).arg( key );
235 QgsSettings settings;
236 settings.beginGroup( baseSettingsKey, QgsSettings::Core );
237 Entry entry;
238 entry.key = key;
239 entry.title = settings.value( QStringLiteral( "title" ) ).toString();
240 entry.imageUrl = settings.value( QStringLiteral( "imageUrl" ) ).toString();
241 entry.content = settings.value( QStringLiteral( "content" ) ).toString();
242 entry.link = settings.value( QStringLiteral( "link" ) ).toString();
243 entry.sticky = settings.value( QStringLiteral( "sticky" ) ).toBool();
244 entry.expiry = settings.value( QStringLiteral( "expiry" ) ).toDateTime();
245 if ( !entry.imageUrl.isEmpty() )
246 {
247 const QString previewDir = QStringLiteral( "%1/previewImages" ).arg( QgsApplication::qgisSettingsDirPath() );
248 const QString imagePath = QStringLiteral( "%1/%2.png" ).arg( previewDir ).arg( entry.key );
249 if ( QFile::exists( imagePath ) )
250 {
251 const QImage img( imagePath );
252 entry.image = QPixmap::fromImage( img );
253 }
254 else
255 {
256 fetchImageForEntry( entry );
257 }
258 }
259 return entry;
260}
261
262void QgsNewsFeedParser::storeEntryInSettings( const QgsNewsFeedParser::Entry &entry )
263{
264 const QString baseSettingsKey = QStringLiteral( "%1/%2" ).arg( mSettingsKey ).arg( entry.key );
265 QgsSettings settings;
266 settings.setValue( QStringLiteral( "%1/title" ).arg( baseSettingsKey ), entry.title, QgsSettings::Core );
267 settings.setValue( QStringLiteral( "%1/imageUrl" ).arg( baseSettingsKey ), entry.imageUrl, QgsSettings::Core );
268 settings.setValue( QStringLiteral( "%1/content" ).arg( baseSettingsKey ), entry.content, QgsSettings::Core );
269 settings.setValue( QStringLiteral( "%1/link" ).arg( baseSettingsKey ), entry.link, QgsSettings::Core );
270 settings.setValue( QStringLiteral( "%1/sticky" ).arg( baseSettingsKey ), entry.sticky, QgsSettings::Core );
271 if ( entry.expiry.isValid() )
272 settings.setValue( QStringLiteral( "%1/expiry" ).arg( baseSettingsKey ), entry.expiry, QgsSettings::Core );
273}
274
275void QgsNewsFeedParser::fetchImageForEntry( const QgsNewsFeedParser::Entry &entry )
276{
277 // start fetching image
279 connect( fetcher, &QgsNetworkContentFetcher::finished, this, [ = ]
280 {
281 const auto findIter = std::find_if( mEntries.begin(), mEntries.end(), [entry]( const QgsNewsFeedParser::Entry & candidate )
282 {
283 return candidate.key == entry.key;
284 } );
285 if ( findIter != mEntries.end() )
286 {
287 const int entryIndex = static_cast< int >( std::distance( mEntries.begin(), findIter ) );
288
289 QImage img = QImage::fromData( fetcher->reply()->readAll() );
290
291 QSize size = img.size();
292 bool resize = false;
293 if ( size.width() > 250 )
294 {
295 size.setHeight( static_cast< int >( size.height() * static_cast< double >( 250 ) / size.width() ) );
296 size.setWidth( 250 );
297 resize = true;
298 }
299 if ( size.height() > 177 )
300 {
301 size.setWidth( static_cast< int >( size.width() * static_cast< double >( 177 ) / size.height() ) );
302 size.setHeight( 177 );
303 resize = true;
304 }
305 if ( resize )
306 img = img.scaled( size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
307
308 //nicely round corners so users don't get paper cuts
309 QImage previewImage( size, QImage::Format_ARGB32 );
310 previewImage.fill( Qt::transparent );
311 QPainter previewPainter( &previewImage );
312 previewPainter.setRenderHint( QPainter::Antialiasing, true );
313 previewPainter.setRenderHint( QPainter::SmoothPixmapTransform, true );
314 previewPainter.setPen( Qt::NoPen );
315 previewPainter.setBrush( Qt::black );
316 previewPainter.drawRoundedRect( 0, 0, size.width(), size.height(), 8, 8 );
317 previewPainter.setCompositionMode( QPainter::CompositionMode_SourceIn );
318 previewPainter.drawImage( 0, 0, img );
319 previewPainter.end();
320
321 // Save image, so we don't have to fetch it next time
322 const QString previewDir = QStringLiteral( "%1/previewImages" ).arg( QgsApplication::qgisSettingsDirPath() );
323 QDir().mkdir( previewDir );
324 const QString imagePath = QStringLiteral( "%1/%2.png" ).arg( previewDir ).arg( entry.key );
325 previewImage.save( imagePath );
326
327 mEntries[ entryIndex ].image = QPixmap::fromImage( previewImage );
328 this->emit imageFetched( entry.key, mEntries[ entryIndex ].image );
329 }
330 fetcher->deleteLater();
331 } );
332 fetcher->fetchContent( entry.imageUrl, mAuthCfg );
333}
334
335QString QgsNewsFeedParser::keyForFeed( const QString &baseUrl )
336{
337 static const QRegularExpression sRegexp( QStringLiteral( "[^a-zA-Z0-9]" ) );
338 QString res = baseUrl;
339 res = res.replace( sRegexp, QString() );
340 return QStringLiteral( "NewsFeed/%1" ).arg( res );
341}
static QString qgisSettingsDirPath()
Returns the path to the settings directory in user's home dir.
static QgsTaskManager * taskManager()
Returns the application's task manager, used for managing application wide background task handling.
static const QgsSettingsEntryString settingsLocaleUserLocale
Settings entry locale user locale.
static QVariant parseJson(const std::string &jsonString)
Converts JSON jsonString to a QVariant, in case of parsing error an invalid QVariant is returned and ...
Handles HTTP network content fetching in a background task.
QString contentAsString() const
Returns the fetched content as a string.
void fetched()
Emitted when the network content has been fetched, regardless of whether the fetch was successful or ...
QNetworkReply * reply()
Returns the network reply.
HTTP network content fetcher.
void finished()
Emitted when content has loaded.
QNetworkReply * reply()
Returns a reference to the network reply.
void fetchContent(const QUrl &url, const QString &authcfg=QString())
Fetches content from a remote URL and handles redirects.
Represents a single entry from a news feed.
QString content
HTML content of news entry.
bool sticky
true if entry is "sticky" and should always be shown at the top
QUrl link
Optional URL link for entry.
QString imageUrl
Optional URL for image associated with entry.
QDateTime expiry
Optional auto-expiry time for entry.
int key
Unique entry identifier.
QString title
Entry title.
static const QgsSettingsEntryDouble settingsFeedLongitude
Settings entry feed longitude.
void dismissEntry(int key)
Dismisses an entry with matching key.
void fetch()
Fetches new entries from the feed's URL.
static const QgsSettingsEntryInteger settingsFeedLastFetchTime
Settings entry last fetch time.
static const QgsSettingsEntryDouble settingsFeedLatitude
Settings entry feed latitude.
void fetched(const QList< QgsNewsFeedParser::Entry > &entries)
Emitted when entries have fetched from the feed.
QString authcfg() const
Returns the authentication configuration for the parser.
QgsNewsFeedParser(const QUrl &feedUrl, const QString &authcfg=QString(), QObject *parent=nullptr)
Constructor for QgsNewsFeedParser, parsing the specified feedUrl.
void dismissAll()
Dismisses all current news items.
static QString keyForFeed(const QString &baseUrl)
Returns the settings key used for a feed with the given baseUrl.
void entryAdded(const QgsNewsFeedParser::Entry &entry)
Emitted whenever a new entry is available from the feed (as a result of a call to fetch()).
void imageFetched(int key, const QPixmap &pixmap)
Emitted when the image attached to the entry with the specified key has been fetched and is now avail...
static const QgsSettingsEntryString settingsFeedLanguage
Settings entry feed language.
QList< QgsNewsFeedParser::Entry > entries() const
Returns a list of existing entries in the feed.
bool exists(const QString &dynamicKeyPart=QString()) const
Returns true if the settings is contained in the underlying QSettings.
T valueWithDefaultOverride(const T &defaultValueOverride, const QString &dynamicKeyPart=QString()) const
Returns the settings value with a defaultValueOverride and with an optional dynamicKeyPart.
T value(const QString &dynamicKeyPart=QString()) const
Returns settings value.
bool setValue(T value, const QString &dynamicKeyPart=QString()) const
Set settings value.
T value(const QString &dynamicKeyPart=QString()) const
Returns settings value.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
QStringList childGroups() const
Returns a list of all key top-level groups that contain keys that can be read using the QSettings obj...
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void beginGroup(const QString &prefix, QgsSettings::Section section=QgsSettings::NoSection)
Appends prefix to the current group.
Definition: qgssettings.cpp:90
void remove(const QString &key, QgsSettings::Section section=QgsSettings::NoSection)
Removes the setting key and any sub-settings of key in a section.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
long addTask(QgsTask *task, int priority=0)
Adds a task to the manager.
@ CanCancel
Task can be canceled.
@ CancelWithoutPrompt
Task can be canceled without any users prompts, e.g. when closing a project or QGIS.
@ Silent
Don't show task updates (such as completion/failure messages) as operating-system level notifications...
void setDescription(const QString &description)
Sets the task's description.
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
Definition: qgis.h:2466
#define QgsSetRequestInitiatorClass(request, _class)