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