QGIS API Documentation  3.26.3-Buenos Aires (65e4edfdad)
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 
31 QgsNewsFeedParser::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 
85 QList<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 
170 void 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 
206 void 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 
232 QgsNewsFeedParser::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 
262 void 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 
275 void 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 
335 QString 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 }
QgsNewsFeedParser::dismissAll
void dismissAll()
Dismisses all current news items.
Definition: qgsnewsfeedparser.cpp:124
QgsSettings::remove
void remove(const QString &key, QgsSettings::Section section=QgsSettings::NoSection)
Removes the setting key and any sub-settings of key in a section.
Definition: qgssettings.cpp:192
QgsSettings::value
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
Definition: qgssettings.cpp:161
QgsTask::CancelWithoutPrompt
@ CancelWithoutPrompt
Task can be canceled without any users prompts, e.g. when closing a project or QGIS.
Definition: qgstaskmanager.h:75
QgsTask::setDescription
void setDescription(const QString &description)
Sets the task's description.
Definition: qgstaskmanager.cpp:51
QgsNewsFeedParser::settingsFeedLatitude
static const QgsSettingsEntryDouble settingsFeedLatitude
Settings entry feed latitude.
Definition: qgsnewsfeedparser.h:120
QgsSettings::Core
@ Core
Definition: qgssettings.h:70
QgsSettingsEntryByReference::valueWithDefaultOverride
T valueWithDefaultOverride(const T &defaultValueOverride, const QString &dynamicKeyPart=QString()) const
Returns the settings value with a defaultValueOverride and with an optional dynamicKeyPart.
Definition: qgssettingsentry.h:390
QgsNetworkContentFetcherTask::fetched
void fetched()
Emitted when the network content has been fetched, regardless of whether the fetch was successful or ...
QgsNetworkContentFetcherTask::reply
QNetworkReply * reply()
Returns the network reply.
Definition: qgsnetworkcontentfetchertask.cpp:88
QgsNetworkContentFetcherTask
Handles HTTP network content fetching in a background task.
Definition: qgsnetworkcontentfetchertask.h:47
qgis.h
QgsSettings
This class is a composition of two QSettings instances:
Definition: qgssettings.h:61
qgsnetworkcontentfetchertask.h
QgsSettingsEntryBase::exists
bool exists(const QString &dynamicKeyPart=QString()) const
Returns true if the settings is contained in the underlying QSettings.
Definition: qgssettingsentry.cpp:173
qgsnewsfeedparser.h
QgsSetRequestInitiatorClass
#define QgsSetRequestInitiatorClass(request, _class)
Definition: qgsnetworkaccessmanager.h:45
QgsNewsFeedParser::entryAdded
void entryAdded(const QgsNewsFeedParser::Entry &entry)
Emitted whenever a new entry is available from the feed (as a result of a call to fetch()).
qgsDoubleToString
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
Definition: qgis.h:2204
QgsNewsFeedParser::Entry::expiry
QDateTime expiry
Optional auto-expiry time for entry.
Definition: qgsnewsfeedparser.h:73
QgsTaskManager::addTask
long addTask(QgsTask *task, int priority=0)
Adds a task to the manager.
Definition: qgstaskmanager.cpp:420
qgsapplication.h
QgsTask::CanCancel
@ CanCancel
Task can be canceled.
Definition: qgstaskmanager.h:74
QgsTask::Silent
@ Silent
Don't show task updates (such as completion/failure messages) as operating-system level notifications...
Definition: qgstaskmanager.h:77
QgsApplication::qgisSettingsDirPath
static QString qgisSettingsDirPath()
Returns the path to the settings directory in user's home dir.
Definition: qgsapplication.cpp:1099
QgsNewsFeedParser::Entry::sticky
bool sticky
true if entry is "sticky" and should always be shown at the top
Definition: qgsnewsfeedparser.h:70
QgsNetworkContentFetcher
HTTP network content fetcher. A simple method for fetching remote HTTP content and converting the con...
Definition: qgsnetworkcontentfetcher.h:39
QgsSettingsEntryByValue::setValue
bool setValue(T value, const QString &dynamicKeyPart=QString()) const
Set settings value.
Definition: qgssettingsentry.h:565
QgsNewsFeedParser::settingsFeedLongitude
static const QgsSettingsEntryDouble settingsFeedLongitude
Settings entry feed longitude.
Definition: qgsnewsfeedparser.h:122
QgsNetworkContentFetcher::fetchContent
void fetchContent(const QUrl &url, const QString &authcfg=QString())
Fetches content from a remote URL and handles redirects.
Definition: qgsnetworkcontentfetcher.cpp:37
QgsNewsFeedParser::fetch
void fetch()
Fetches new entries from the feed's URL.
Definition: qgsnewsfeedparser.cpp:138
qgsnetworkaccessmanager.h
QgsNewsFeedParser::settingsFeedLanguage
static const QgsSettingsEntryString settingsFeedLanguage
Settings entry feed language.
Definition: qgsnewsfeedparser.h:118
QgsNewsFeedParser::fetched
void fetched(const QList< QgsNewsFeedParser::Entry > &entries)
Emitted when entries have fetched from the feed.
QgsApplication::taskManager
static QgsTaskManager * taskManager()
Returns the application's task manager, used for managing application wide background task handling.
Definition: qgsapplication.cpp:2300
QgsNetworkContentFetcher::reply
QNetworkReply * reply()
Returns a reference to the network reply.
Definition: qgsnetworkcontentfetcher.cpp:89
QgsApplication::settingsLocaleUserLocale
static const QgsSettingsEntryString settingsLocaleUserLocale
Settings entry locale user locale.
Definition: qgsapplication.h:1029
QgsSettings::setValue
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
Definition: qgssettings.cpp:279
QgsNewsFeedParser::Entry::link
QUrl link
Optional URL link for entry.
Definition: qgsnewsfeedparser.h:67
QgsNewsFeedParser::settingsFeedLastFetchTime
static const QgsSettingsEntryInteger settingsFeedLastFetchTime
Settings entry last fetch time.
Definition: qgsnewsfeedparser.h:116
QgsNetworkContentFetcherTask::contentAsString
QString contentAsString() const
Returns the fetched content as a string.
Definition: qgsnetworkcontentfetchertask.cpp:93
QgsNewsFeedParser::Entry::title
QString title
Entry title.
Definition: qgsnewsfeedparser.h:55
QgsNewsFeedParser::imageFetched
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...
QgsSettings::beginGroup
void beginGroup(const QString &prefix, QgsSettings::Section section=QgsSettings::NoSection)
Appends prefix to the current group.
Definition: qgssettings.cpp:89
QgsNewsFeedParser::authcfg
QString authcfg() const
Returns the authentication configuration for the parser.
Definition: qgsnewsfeedparser.cpp:133
QgsSettingsEntryByReference::value
T value(const QString &dynamicKeyPart=QString()) const
Returns settings value.
Definition: qgssettingsentry.h:379
QgsNewsFeedParser::Entry::content
QString content
HTML content of news entry.
Definition: qgsnewsfeedparser.h:64
QgsNewsFeedParser::Entry
Represents a single entry from a news feed.
Definition: qgsnewsfeedparser.h:47
qgssettings.h
QgsNetworkContentFetcher::finished
void finished()
Emitted when content has loaded.
QgsNewsFeedParser::keyForFeed
static QString keyForFeed(const QString &baseUrl)
Returns the settings key used for a feed with the given baseUrl.
Definition: qgsnewsfeedparser.cpp:335
QgsNewsFeedParser::Entry::imageUrl
QString imageUrl
Optional URL for image associated with entry.
Definition: qgsnewsfeedparser.h:58
qgsnetworkcontentfetcher.h
qgsjsonutils.h
qgslogger.h
QgsNewsFeedParser::dismissEntry
void dismissEntry(int key)
Dismisses an entry with matching key.
Definition: qgsnewsfeedparser.cpp:90
QgsNewsFeedParser::entries
QList< QgsNewsFeedParser::Entry > entries() const
Returns a list of existing entries in the feed.
Definition: qgsnewsfeedparser.cpp:85
QgsNewsFeedParser::Entry::key
int key
Unique entry identifier.
Definition: qgsnewsfeedparser.h:52
QgsSettings::childGroups
QStringList childGroups() const
Returns a list of all key top-level groups that contain keys that can be read using the QSettings obj...
Definition: qgssettings.cpp:136
QgsSettingsEntryByValue::value
T value(const QString &dynamicKeyPart=QString()) const
Returns settings value.
Definition: qgssettingsentry.h:520
qgsmessagelog.h
QgsJsonUtils::parseJson
static QVariant parseJson(const std::string &jsonString)
Converts JSON jsonString to a QVariant, in case of parsing error an invalid QVariant is returned and ...
Definition: qgsjsonutils.cpp:456
QgsNewsFeedParser::QgsNewsFeedParser
QgsNewsFeedParser(const QUrl &feedUrl, const QString &authcfg=QString(), QObject *parent=nullptr)
Constructor for QgsNewsFeedParser, parsing the specified feedUrl.
Definition: qgsnewsfeedparser.cpp:31