Skip to content

Commit a94c016

Browse files
author
Martin Rotter
committed
Fixed #265.
1 parent c53b4bb commit a94c016

14 files changed

+466
-273
lines changed

resources/desktop/com.github.rssguard.appdata.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
<url type="donation">https://martinrotter.github.io/donate/</url>
3131
<content_rating type="oars-1.1" />
3232
<releases>
33-
<release version="3.8.4" date="2021-02-02"/>
33+
<release version="3.8.4" date="2021-02-03"/>
3434
</releases>
3535
<content_rating type="oars-1.0">
3636
<content_attribute id="violence-cartoon">none</content_attribute>

resources/docs/Feed-formats.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,31 @@ RSS Guard is a modular application which supports plugins. It offers well-mainta
44
* [Tiny Tiny RSS](https://tt-rss.org) plugin: Adds ability to synchronize messages with TT-RSS instances, either self-hosted or via 3rd-party external service.
55
* [Inoreader](https://www.inoreader.com) plugin: Adds ability to synchronize messages with Inoreader. All you need to do is create free account on their website and start rocking.
66
* [Nextcloud News](https://apps.nextcloud.com/apps/news) plugin: Nextcloud News is a Nextcloud app which adds feed reader abilities into your Nextcloud instances.
7-
* Google Reader API plugin: This plugin was added in RSS Guard 3.9.0 and offers two-way synchronization with services which implement Google Reader API. At this point, plugin was tested and works with Bazqux, The Old Reader and FreshRSS.
7+
* [Google Reader API](https://rss-sync.github.io/Open-Reader-API/resources/#unofficial-google-reader-documentation) plugin: This plugin was added in RSS Guard 3.9.0 and offers two-way synchronization with services which implement Google Reader API. At this point, plugin was tested and works with Bazqux, The Old Reader and FreshRSS.
88
* [Gmail](https://www.google.com/gmail) plugin: Yes, you are reading it right. RSS Guard can be used as very lightweight and simple e-mail client. This plugins uses [Gmail API](https://developers.google.com/gmail/api) and offers even e-mail sending.
99

10-
All plugins share almost all core RSS Guard's features, including labels, recycle bins, podcasts fetching or newspaper view. They are implemented in a very transparent way, making it easy to maintain them or add new ones.
10+
> All plugins share almost all core RSS Guard's features, including labels, recycle bins, podcasts fetching or newspaper view. They are implemented in a very transparent way, making it easy to maintain them or add new ones.
1111
12-
Usually, plugins have some exclusive functionality, for example Gmail plugin allows user to send e-mail messages. This extra functionality is always accessible via plugin's context menu and also via main menu.
12+
Usually, plugins have some exclusive functionality, for example Gmail plugin allows user to send e-mail messages or reply to existing ones. This extra functionality is always accessible via plugin's context menu and also via main menu.
1313

1414
<img src="images/gmail-context-menu.png" width="80%">
1515

1616
If there is interest in other plugins, you might write one yourself or if many people are interested then I might write it for you, even commercially if we make proper arrangements.
1717

18+
## Plugin API
19+
RSS Guard offers simple `C++` API for creating new service plugins. All base API classes are in folder [`abstract`](https://github.com/martinrotter/rssguard/tree/master/src/librssguard/services/abstract). User must subclass and implement all interface classes:
20+
21+
| Class | Purpose |
22+
|-------|---------|
23+
| `ServiceEntryPoint` | Very base class which provides basic information about the plugin name, author etc. It also provides methods which are called when new account should be created and when existing accounts should be loaded from database. |
24+
| `ServiceRoot` | This is the core "account" class which represents account node in feed's list and offers interface for all critical functionality of a plugin, including handlers which are called when starting/stoping a plugin, marking messages read/unread/starred/deleted, (de)assigning labels etc. |
25+
26+
API is reasonably simple to understand but relatively large. Sane default behavior is employed where it makes sense.
27+
28+
Perhaps the best approach to use when writing new plugin is to copy [existing](https://github.com/martinrotter/rssguard/tree/master/src/librssguard/services/greader) one and start from there.
29+
30+
Note that RSS Guard can support loading of plugins from external libraries (dll, so, etc.) but the functionality must be polished because so far all plugins are directly bundled into the app as no one really requested run-time loading of plugins so far.
31+
1832
## Features found exclusively in `standard RSS` plugin
1933
Standard plugin in RSS Guard offers some features which are specific to it. Of course it supports all news syndication formats which are nowadays used:
2034
* RSS 0.90, 0.91, 0.92, 1.0 (also known as *RDF*), 2.0.
@@ -30,10 +44,11 @@ OPML files can be exported/imported in simple dialog.
3044

3145
You just select output file (in case of OPML export), check desired feeds and hit `Export to file`.
3246

33-
### Websites scraping and other related advanced features
34-
RSS Guard 3.9.0+ offers extra advanced features which were inspired by [Liferea](https://lzone.de/liferea/).
47+
### Websites scraping and related advanced features
48+
49+
> **Only proceed if you consider yourself as power user and you know you are doing!**
3550
36-
**Only proceed if you consider yourself as power user and you know you are doing!**
51+
RSS Guard 3.9.0+ offers extra advanced features which are inspired by [Liferea](https://lzone.de/liferea/).
3752

3853
You can select source type of each feed. If you select `URL`, then RSS Guard simply downloads feed file from given location.
3954

@@ -43,13 +58,15 @@ However, if you choose `Script` option, then you cannot provide URL of your feed
4358

4459
Any errors in your script must be written to **error output**.
4560

46-
Note that you must provide full execution line to your custom script, including interpreter binary path and name and all that must be written in special format `<interpreter>#<arguments>`. The `#` character is there to separate interpreter from its arguments. Interpreter must be provided in all cases, arguments do not have to be. For example `bash.exe#` is valid execution line, as well as `bash#-C "cat feed.atom"`. Some examples of valid and tested execution lines are:
61+
Note that you must provide full execution line to your custom script, including interpreter binary path and name and all that must be written in special format `<interpreter>#<arguments>`. The `#` character is there to separate interpreter from its arguments.
62+
63+
Interpreter must be provided in all cases, arguments do not have to be. For example `bash.exe#` is valid execution line, as well as `bash#-C "cat feed.atom"`. Note the difference in interpreter's binary name suffix. Some examples of valid and tested execution lines are:
4764

4865
| Command | Explanation |
4966
|---------|-------------|
5067
| `bash#-c "curl 'https://github.com/martinrotter.atom'"` | Downloads ATOM feed file with Bash and Curl. |
5168
| `Powershell#"Invoke-WebRequest 'https://github.com/martinrotter.atom' | Select-Object -ExpandProperty Content"` | Downloads ATOM feed file with Powershell. |
52-
| `php#tweeper.php https://twitter.com/NSACareers` | Downloads RSS feed file with [Tweeper](https://git.ao2.it/tweeper.git/). Tweeper is utility which is able to produce RSS feed from Twitter. |
69+
| `php#tweeper.php -v 0 https://twitter.com/NSACareers` | Downloads RSS feed file with [Tweeper](https://git.ao2.it/tweeper.git/). Tweeper is utility which is able to produce RSS feed from Twitter. |
5370

5471
<img src="images/scrape-source.png" width="50%">
5572

resources/scripts/7za

src/librssguard/exceptions/applicationexception.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ ApplicationException::ApplicationException(QString message) : m_message(std::mov
77
QString ApplicationException::message() const {
88
return m_message;
99
}
10+
11+
void ApplicationException::setMessage(const QString& message) {
12+
m_message = message;
13+
}

src/librssguard/exceptions/applicationexception.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ class ApplicationException {
1111

1212
QString message() const;
1313

14+
protected:
15+
void setMessage(const QString& message);
16+
1417
private:
1518
QString m_message;
1619
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// For license of this file, see <project-root-folder>/LICENSE.md.
2+
3+
#include "exceptions/scriptexception.h"
4+
5+
#include "definitions/definitions.h"
6+
7+
ScriptException::ScriptException(Reason reason, QString message) : ApplicationException(message), m_reason(reason) {
8+
if (message.isEmpty()) {
9+
setMessage(messageForReason(reason));
10+
}
11+
else if (reason == ScriptException::Reason::InterpreterError ||
12+
reason == ScriptException::Reason::OtherError) {
13+
setMessage(messageForReason(reason) + QSL(": '%1'").arg(message));
14+
}
15+
}
16+
17+
ScriptException::Reason ScriptException::reason() const {
18+
return m_reason;
19+
}
20+
21+
QString ScriptException::messageForReason(ScriptException::Reason reason) const {
22+
switch (reason) {
23+
case ScriptException::Reason::ExecutionLineInvalid:
24+
return tr("script line is not well-formed");
25+
26+
case ScriptException::Reason::InterpreterError:
27+
return tr("script threw an error");
28+
29+
case ScriptException::Reason::InterpreterNotFound:
30+
return tr("script's interpreter was not found");
31+
32+
case ScriptException::Reason::InterpreterTimeout:
33+
return tr("script execution took too long");
34+
35+
case ScriptException::Reason::OtherError:
36+
default:
37+
return tr("unknown error");
38+
}
39+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// For license of this file, see <project-root-folder>/LICENSE.md.
2+
3+
#ifndef SCRIPTEXCEPTION_H
4+
#define SCRIPTEXCEPTION_H
5+
6+
#include "exceptions/applicationexception.h"
7+
8+
#include <QCoreApplication>
9+
10+
class ScriptException : public ApplicationException {
11+
Q_DECLARE_TR_FUNCTIONS(ScriptException)
12+
13+
public:
14+
enum class Reason {
15+
ExecutionLineInvalid,
16+
InterpreterNotFound,
17+
InterpreterError,
18+
InterpreterTimeout,
19+
OtherError
20+
};
21+
22+
explicit ScriptException(Reason reason = Reason::OtherError, QString message = QString());
23+
24+
Reason reason() const;
25+
26+
private:
27+
QString messageForReason(Reason reason) const;
28+
29+
private:
30+
Reason m_reason;
31+
};
32+
33+
#endif // SCRIPTEXCEPTION_H

src/librssguard/librssguard.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ HEADERS += core/feeddownloader.h \
5050
exceptions/applicationexception.h \
5151
exceptions/filteringexception.h \
5252
exceptions/ioexception.h \
53+
exceptions/scriptexception.h \
5354
gui/baselineedit.h \
5455
gui/basetoolbar.h \
5556
gui/colortoolbutton.h \
@@ -221,6 +222,7 @@ SOURCES += core/feeddownloader.cpp \
221222
exceptions/applicationexception.cpp \
222223
exceptions/filteringexception.cpp \
223224
exceptions/ioexception.cpp \
225+
exceptions/scriptexception.cpp \
224226
gui/baselineedit.cpp \
225227
gui/basetoolbar.cpp \
226228
gui/colortoolbutton.cpp \

src/librssguard/services/standard/gui/formstandardfeeddetails.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,17 @@ int FormStandardFeedDetails::addEditFeed(StandardFeed* input_feed, RootItem* par
4747
}
4848

4949
void FormStandardFeedDetails::guessFeed() {
50-
m_standardFeedDetails->guessFeed(m_standardFeedDetails->m_ui.m_txtSource->lineEdit()->text(),
50+
m_standardFeedDetails->guessFeed(m_standardFeedDetails->sourceType(),
51+
m_standardFeedDetails->m_ui.m_txtSource->lineEdit()->text(),
52+
m_standardFeedDetails->m_ui.m_txtPostProcessScript->lineEdit()->text(),
5153
m_authDetails->m_txtUsername->lineEdit()->text(),
5254
m_authDetails->m_txtPassword->lineEdit()->text());
5355
}
5456

5557
void FormStandardFeedDetails::guessIconOnly() {
56-
m_standardFeedDetails->guessIconOnly(m_standardFeedDetails->m_ui.m_txtSource->lineEdit()->text(),
58+
m_standardFeedDetails->guessIconOnly(m_standardFeedDetails->sourceType(),
59+
m_standardFeedDetails->m_ui.m_txtSource->lineEdit()->text(),
60+
m_standardFeedDetails->m_ui.m_txtPostProcessScript->lineEdit()->text(),
5761
m_authDetails->m_txtUsername->lineEdit()->text(),
5862
m_authDetails->m_txtPassword->lineEdit()->text());
5963
}

src/librssguard/services/standard/gui/standardfeeddetails.cpp

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -107,53 +107,63 @@ StandardFeedDetails::StandardFeedDetails(QWidget* parent) : QWidget(parent) {
107107
onPostProcessScriptChanged({});
108108
}
109109

110-
void StandardFeedDetails::guessIconOnly(const QString& url, const QString& username,
110+
void StandardFeedDetails::guessIconOnly(StandardFeed::SourceType source_type, const QString& source,
111+
const QString& post_process_script, const QString& username,
111112
const QString& password, const QNetworkProxy& custom_proxy) {
112-
QPair<StandardFeed*, QNetworkReply::NetworkError> result = StandardFeed::guessFeed(url,
113-
username,
114-
password,
115-
custom_proxy);
116-
117-
if (result.first != nullptr) {
113+
bool result;
114+
StandardFeed* metadata = StandardFeed::guessFeed(source_type,
115+
source,
116+
post_process_script,
117+
&result,
118+
username,
119+
password,
120+
custom_proxy);
121+
122+
if (metadata != nullptr) {
118123
// Icon or whole feed was guessed.
119-
m_ui.m_btnIcon->setIcon(result.first->icon());
124+
m_ui.m_btnIcon->setIcon(metadata->icon());
120125

121-
if (result.second == QNetworkReply::NoError) {
126+
if (result) {
122127
m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Ok,
123128
tr("Icon fetched successfully."),
124129
tr("Icon metadata fetched."));
125130
}
126131
else {
127132
m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Warning,
128-
tr("Result: %1.").arg(NetworkFactory::networkErrorText(result.second)),
133+
tr("Icon metadata not fetched."),
129134
tr("Icon metadata not fetched."));
130135
}
131136

132137
// Remove temporary feed object.
133-
delete result.first;
138+
delete metadata;
134139
}
135140
else {
136141
// No feed guessed, even no icon available.
137142
m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Error,
138-
tr("Error: %1.").arg(NetworkFactory::networkErrorText(result.second)),
143+
tr("No icon fetched."),
139144
tr("No icon fetched."));
140145
}
141146
}
142147

143-
void StandardFeedDetails::guessFeed(const QString& url, const QString& username,
148+
void StandardFeedDetails::guessFeed(StandardFeed::SourceType source_type, const QString& source,
149+
const QString& post_process_script, const QString& username,
144150
const QString& password, const QNetworkProxy& custom_proxy) {
145-
QPair<StandardFeed*, QNetworkReply::NetworkError> result = StandardFeed::guessFeed(url,
146-
username,
147-
password,
148-
custom_proxy);
149-
150-
if (result.first != nullptr) {
151+
bool result;
152+
StandardFeed* metadata = StandardFeed::guessFeed(source_type,
153+
source,
154+
post_process_script,
155+
&result,
156+
username,
157+
password,
158+
custom_proxy);
159+
160+
if (metadata != nullptr) {
151161
// Icon or whole feed was guessed.
152-
m_ui.m_btnIcon->setIcon(result.first->icon());
153-
m_ui.m_txtTitle->lineEdit()->setText(result.first->title());
154-
m_ui.m_txtDescription->lineEdit()->setText(result.first->description());
155-
m_ui.m_cmbType->setCurrentIndex(m_ui.m_cmbType->findData(QVariant::fromValue((int) result.first->type())));
156-
int encoding_index = m_ui.m_cmbEncoding->findText(result.first->encoding(), Qt::MatchFixedString);
162+
m_ui.m_btnIcon->setIcon(metadata->icon());
163+
m_ui.m_txtTitle->lineEdit()->setText(metadata->title());
164+
m_ui.m_txtDescription->lineEdit()->setText(metadata->description());
165+
m_ui.m_cmbType->setCurrentIndex(m_ui.m_cmbType->findData(QVariant::fromValue((int) metadata->type())));
166+
int encoding_index = m_ui.m_cmbEncoding->findText(metadata->encoding(), Qt::MatchFlag::MatchFixedString);
157167

158168
if (encoding_index >= 0) {
159169
m_ui.m_cmbEncoding->setCurrentIndex(encoding_index);
@@ -162,24 +172,24 @@ void StandardFeedDetails::guessFeed(const QString& url, const QString& username,
162172
m_ui.m_cmbEncoding->setCurrentIndex(m_ui.m_cmbEncoding->findText(DEFAULT_FEED_ENCODING, Qt::MatchFixedString));
163173
}
164174

165-
if (result.second == QNetworkReply::NoError) {
175+
if (result) {
166176
m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Ok,
167177
tr("All metadata fetched successfully."),
168178
tr("Feed and icon metadata fetched."));
169179
}
170180
else {
171181
m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Warning,
172-
tr("Result: %1.").arg(NetworkFactory::networkErrorText(result.second)),
182+
tr("Feed or icon metadata not fetched."),
173183
tr("Feed or icon metadata not fetched."));
174184
}
175185

176186
// Remove temporary feed object.
177-
delete result.first;
187+
delete metadata;
178188
}
179189
else {
180190
// No feed guessed, even no icon available.
181191
m_ui.m_lblFetchMetadata->setStatus(WidgetWithStatus::StatusType::Error,
182-
tr("Error: %1.").arg(NetworkFactory::networkErrorText(result.second)),
192+
tr("No metadata fetched."),
183193
tr("No metadata fetched."));
184194
}
185195
}

0 commit comments

Comments
 (0)