Add support for Tiny Tiny Rss (#27)

* Add support for Tiny Tiny Rss
* Fix copyright notice in ttrssfetcher files
* Fix json format for bulk set as read in ttrss
* Correctly report errors when sending actions for ttrss
* Disable the slow/liked view mode for ttrss
* Systematically clear json data before sending a new command
* Remove debug messages
This commit is contained in:
Renaud Casenave-Péré 2019-02-17 00:36:21 +09:00 committed by Mkiol
parent d9bd6035ca
commit 5fdd9270e1
12 changed files with 1132 additions and 10 deletions

View file

@ -1,6 +1,6 @@
# Kaktus
Multi services mobile feed reader, specially designed to work offline. Supports [Netvibes](http://www.netvibes.com/) and [The Old Reader](https://theoldreader.com/) as a feed aggregators. Works on Sailfish OS & BlackBerry 10 devices.
Multi services mobile feed reader, specially designed to work offline. Supports [Netvibes](http://www.netvibes.com/), [The Old Reader](https://theoldreader.com/) and [Tiny Tiny Rss](https://tt-rss.org) as a feed aggregators. Works on Sailfish OS & BlackBerry 10 devices.
Kaktus is working in sync mode, so network connectivity is not
required all the time. The significant feature is possibility

View file

@ -26,6 +26,7 @@ SOURCES += \
src/oldreaderfetcher.cpp \
src/nvfetcher.cpp \
src/feedlyfetcher.cpp \
src/ttrssfetcher.cpp \
src/networkaccessmanagerfactory.cpp \
src/customnetworkaccessmanager.cpp \
src/iconprovider.cpp
@ -49,6 +50,7 @@ HEADERS += \
src/oldreaderfetcher.h \
src/nvfetcher.h \
src/feedlyfetcher.h \
src/ttrssfetcher.h \
src/networkaccessmanagerfactory.h \
src/customnetworkaccessmanager.h \
feedly.h \
@ -85,4 +87,5 @@ INSTALLS += translations images
DISTFILES += \
qml/*.qml \
qml/ScalableIconButton.qml \
qml/MenuIconItem.qml
qml/MenuIconItem.qml \
qml/TTRssSignInDialog.qml

View file

@ -49,6 +49,7 @@ Page {
ListElement { name: "Netvibes"; iconSource: "nv.png"; type: 1}
ListElement { name: "Old Reader"; iconSource: "oldreader.png"; type: 2}
/*ListElement { name: "Feedly (comming soon)"; iconSource: "feedly.png"; type: 3}*/
ListElement { name: "Tiny Tiny Rss"; iconSource: "ttrss.png"; type: 4}
}
header: PageHeader {
@ -107,6 +108,10 @@ Page {
fetcher.getConnectUrl(20);
//pageStack.replaceAbove(pageStack.previousPage(),Qt.resolvedUrl("FeedlySignInDialog.qml"),{"code": 400});
}
if (type == 4) {
app.reconnectFetcher(4);
pageStack.replaceAbove(pageStack.previousPage(), Qt.resolvedUrl("TTRssSignInDialog.qml"), {"code": 400});
}
}
}

View file

@ -73,7 +73,9 @@ Page {
verticalCenter: parent.verticalCenter
}
source: app.isNetvibes ? "nv.png" :
app.isOldReader ? "oldreader.png" : "feedly.png"
app.isOldReader ? "oldreader.png" :
app.isFeedly ? "feedly.png" :
app.isTTRss ? "ttrss.png" : null
width: Theme.iconSizeMedium
height: Theme.iconSizeMedium
@ -87,7 +89,9 @@ Page {
verticalCenter: parent.verticalCenter
}
text: app.isNetvibes ? "Netvibes":
app.isOldReader ? "Old Reader" : "Feedly"
app.isOldReader ? "Old Reader" :
app.isFeedly ? "Feedly" :
app.isTTRss ? "Tiny Tiny Rss" : null
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignRight
color: Theme.highlightColor
@ -119,11 +123,12 @@ Page {
color: Theme.highlightColor
visible: settings.signedIn
text: settings.signedIn ?
settings.signinType==0 ? settings.getUsername() :
settings.signinType==1 ? "Twitter" :
settings.signinType==2 ? "Facebook" :
settings.signinType==10 ? settings.getUsername() :
settings.signinType==20 ? settings.getProvider() : "" : ""
(settings.signinType==0 ? settings.getUsername() :
settings.signinType==1 ? "Twitter" :
settings.signinType==2 ? "Facebook" :
settings.signinType==10 ? settings.getUsername() :
settings.signinType==20 ? settings.getProvider() :
settings.signinType==30 ? settings.getUsername() : "") : ""
}
}
@ -676,6 +681,7 @@ Page {
iconSource: "image://icons/icon-m-vm4"
}
MenuIconItem {
visible: !app.isTTRss
enabled: app.isNetvibes || (app.isOldReader && settings.showBroadcast)
text: app.isNetvibes ? qsTr("Slow") : qsTr("Liked")
iconSource: app.isNetvibes ? "image://icons/icon-m-vm5" : "image://icons/icon-m-vm6"

View file

@ -0,0 +1,198 @@
/*
Copyright (C) 2014 Michal Kosciesza <michal@mkiol.net>
This file is part of Kaktus.
Kaktus is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Kaktus is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Kaktus. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.0
import Sailfish.Silica 1.0
Dialog {
id: root
property bool showBar: false
property int code
canAccept: url.text != "" && user.text != "" && password.text != ""
allowedOrientations: {
switch (settings.allowedOrientations) {
case 1:
return Orientation.Portrait;
case 2:
return Orientation.Landscape;
}
return Orientation.Landscape | Orientation.Portrait;
}
ActiveDetector {}
SilicaFlickable {
anchors {left: parent.left; right: parent.right }
anchors {top: parent.top}
height: app.flickHeight
clip: true
contentHeight: content.height
Column {
id: content
anchors {
left: parent.left
right: parent.right
}
spacing: Theme.paddingSmall
DialogHeader {
acceptText : qsTr("Sign in")
}
Item {
anchors { left: parent.left; right: parent.right}
height: Math.max(icon.height, label.height)
Image {
id: icon
anchors { right: label.left; rightMargin: Theme.paddingMedium }
source: "ttrss.png"
width: Theme.iconSizeMedium
height: Theme.iconSizeMedium
}
Label {
id: label
anchors { right: parent.right; rightMargin: Theme.paddingLarge}
text: qsTr("Tiny Tiny Rss")
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignRight
color: Theme.highlightColor
font.pixelSize: Theme.fontSizeSmall
y: Theme.paddingSmall/2
}
}
Item {
height: Theme.paddingMedium
width: Theme.paddingMedium
}
PaddedLabel {
text: qsTr("Enter server url and credentials below.")
}
Item {
height: Theme.paddingMedium
width: Theme.paddingMedium
}
TextField {
id: url
anchors.left:parent.left; anchors.right: parent.right
inputMethodHints: Qt.ImhEmailCharactersOnly | Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
placeholderText: qsTr("Enter the url of your server here!")
label: qsTr("Server Url")
Component.onCompleted: {
text = settings.getUrl();
}
EnterKey.iconSource: "image://theme/icon-m-enter-close"
EnterKey.onClicked: {
Qt.inputMethod.hide();
}
}
TextField {
id: user
anchors.left: parent.left; anchors.right: parent.right
inputMethodHints: Qt.ImhEmailCharactersOnly| Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
placeholderText: qsTr("Enter username here!")
label: qsTr("Username")
Component.onCompleted: {
text = settings.getUsername();
}
EnterKey.iconSource: "image://theme/icon-m-enter-close"
EnterKey.onClicked: {
Qt.inputMethod.hide();
}
}
TextField {
id: password
anchors.left: parent.left; anchors.right: parent.right
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText | Qt.ImhSensitiveData
echoMode: TextInput.Password
placeholderText: qsTr("Enter password here!")
label: qsTr("Password")
EnterKey.iconSource: url.text!=="" && user.text!=="" ? "image://theme/icon-m-enter-accept" : "image://theme/icon-m-enter-close"
EnterKey.onClicked: {
Qt.inputMethod.hide();
if (url.text!=="" && user.text!=="")
root.accept();
}
}
Item {
height: Theme.itemSizeLarge
width: Theme.itemSizeLarge
}
}
}
function validateEmail(email) {
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
onAccepted: {
settings.setUrl(url.text);
settings.setUsername(user.text);
settings.setPassword(password.text);
m.doInit = settings.signinType != 30;
settings.signinType = 30;
if (code == 0) {
fetcher.checkCredentials();
} else {
if (! dm.busy)
dm.cancel();
m.doUpdate = true;
}
}
// trick!
QtObject {
id: m
property bool doUpdate: false
property bool doInit: false
}
Component.onDestruction: {
if (m.doUpdate) {
if (m.doInit)
fetcher.init();
else
fetcher.update();
}
}
}

View file

@ -31,6 +31,7 @@ ApplicationWindow {
readonly property bool isNetvibes: settings.signinType >= 0 && settings.signinType < 10
readonly property bool isOldReader: settings.signinType >= 10 && settings.signinType < 20
readonly property bool isFeedly: settings.signinType >= 20 && settings.signinType < 30
readonly property bool isTTRss: settings.signinType >= 30 && settings.signinType < 40
readonly property variant _cache: cache
readonly property int stdHeight: orientation==Orientation.Portrait ? Theme.itemSizeMedium : 0.8 * Theme.itemSizeMedium
@ -76,6 +77,8 @@ ApplicationWindow {
reconnectFetcher(2);
else if (type < 30)
reconnectFetcher(3);
else if (type < 40)
reconnectFetcher(4);
}
utils.setRootModel();
@ -326,6 +329,10 @@ ApplicationWindow {
pageStack.push(Qt.resolvedUrl("FeedlySignInDialog.qml"),{"code": code});
return;
}
if (type < 40) {
pageStack.push(Qt.resolvedUrl("TTRssSignInDialog.qml"),{"code": code});
return;
}
} else {
// Unknown error

BIN
sailfish/qml/ttrss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -415,6 +415,16 @@ bool Settings::getAutoDownloadOnUpdate()
return settings.value("autodownloadonupdate", true).toBool();
}
void Settings::setUrl(const QString &value)
{
settings.setValue("url", value);
}
QString Settings::getUrl()
{
return settings.value("url", "").toString();
}
void Settings::setUsername(const QString &value)
{
settings.setValue("username", value);

View file

@ -212,6 +212,7 @@ public:
// 10 - Oldreader
// 20 - Feedly
// 22 - Feedly with FB
// 30 - Tiny Tiny Rss
void setSigninType(int);
int getSigninType();
@ -226,7 +227,9 @@ public:
Q_INVOKABLE void setRefreshCookie(const QString &value);
Q_INVOKABLE QString getRefreshCookie();
// Username & Password
// Url & Username & Password
Q_INVOKABLE void setUrl(const QString &value);
Q_INVOKABLE QString getUrl();
Q_INVOKABLE void setUsername(const QString &value);
Q_INVOKABLE QString getUsername();
Q_INVOKABLE void setPassword(const QString &value);

View file

@ -0,0 +1,750 @@
/*
Copyright (C) 2019 Michal Kosciesza <michal@mkiol.net>
Copyright (C) 2019 Renaud Casenave-Péré <renaud@casenave-pere.fr>
This file is part of Kaktus.
Kaktus is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Kaktus is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Kaktus. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QRegExp>
#include <QtCore/qmath.h>
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
#include <QJsonDocument>
#endif
#include "ttrssfetcher.h"
#include "settings.h"
#include "downloadmanager.h"
#include "utils.h"
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
#define FETCHER_SLOT(callback) &TTRssFetcher::callback
#else
#define FETCHER_SLOT(callback) SLOT(callback)
#endif
TTRssFetcher::TTRssFetcher(QObject *parent) :
Fetcher(parent),
currentCommand(NULL),
currentJob(Idle)
{
}
TTRssFetcher::~TTRssFetcher()
{
}
void TTRssFetcher::signIn()
{
Settings *s = Settings::instance();
if (sessionId != "") {
prepareUploadActions();
return;
}
QString url = s->getUrl();
QString password = s->getPassword();
QString username = s->getUsername();
int type = s->getSigninType();
switch (type) {
case 30:
{
if (password == "" || username == "" || url == "") {
qWarning() << "TTRss credentials are invalid!";
if (busyType == Fetcher::CheckingCredentials)
emit errorCheckingCredentials(400);
else
emit error(400);
setBusy(false);
return;
}
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
QJsonObject params;
params["user"] = username;
params["password"] = password;
#else
QString params = "\"user\":\"" + username + "\",\"password\":\"" + password.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
#endif
sendApiCall("login", params, FETCHER_SLOT(finishedSignIn));
break;
}
default:
qWarning() << "Invalid sign in type!";
emit error(500);
setBusy(false);
return;
}
}
void TTRssFetcher::finishedSignIn()
{
Settings *s = Settings::instance();
if (!processResponse()) {
s->setSignedIn(false);
sessionId = "";
apiLevel = 0;
return;
} else {
s->setSignedIn(true);
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
sessionId = jsonObj["content"].toObject()["session_id"].toString();
apiLevel = jsonObj["content"].toObject()["api_level"].toInt();
#else
sessionId = jsonObj["content"].toMap()["session_id"].toString();
apiLevel = jsonObj["content"].toMap()["api_level"].toInt();
#endif
if (busyType == Fetcher::CheckingCredentials) {
emit credentialsValid();
setBusy(false);
} else {
sendApiCall("getConfig", FETCHER_SLOT(finishedConfig));
}
}
}
void TTRssFetcher::finishedConfig()
{
if (!processResponse()) {
return;
}
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
iconsUrl = jsonObj["content"].toObject()["icons_url"].toString();
#else
iconsUrl = jsonObj["content"].toMap()["icons_url"].toString();
#endif
prepareUploadActions();
}
void TTRssFetcher::startFetching()
{
Settings *s = Settings::instance();
if (!s->db->makeBackup ()) {
qWarning() << "Unable to make DB backup!";
emit error(506);
setBusy(false);
return;
}
s->db->cleanDashboards();
DatabaseManager::Dashboard d;
d.id = "ttrss";
d.name = "Default";
d.title = "Default";
d.description = "Tiny Tiny Rss default dashboard";
s->db->writeDashboard(d);
s->setDashboardInUse(d.id);
s->db->cleanTabs();
s->db->cleanStreams();
s->db->cleanModules();
if(busyType == Fetcher::Initiating) {
s->db->cleanCache();
s->db->cleanEntries();
}
commandList.clear();
commandList.append(&TTRssFetcher::fetchCategories);
commandList.append(&TTRssFetcher::fetchFeeds);
commandList.append(&TTRssFetcher::fetchStream);
commandList.append(&TTRssFetcher::fetchStarredStream);
commandList.append(&TTRssFetcher::fetchPublishedStream);
commandList.append(&TTRssFetcher::pruneOld);
proggressTotal = commandList.size() + s->getRetentionDays();
proggress = 0;
lastDate = 0;
offset = 0;
callNextCmd();
}
void TTRssFetcher::fetchCategories()
{
sendApiCall("getCategories", FETCHER_SLOT(finishedCategories));
}
void TTRssFetcher::finishedCategories()
{
if (!processResponse()) {
return;
}
startJob(StoreCategories);
}
void TTRssFetcher::fetchFeeds()
{
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
QJsonObject params;
params["cat_id"] = -3;
#else
QString params = "\"cat_id\":-3";
#endif
sendApiCall("getFeeds", params, FETCHER_SLOT(finishedFeeds));
}
void TTRssFetcher::finishedFeeds()
{
if (!processResponse()) {
return;
}
startJob(StoreFeeds);
}
void TTRssFetcher::fetchStream()
{
Settings *s = Settings::instance();
if (offset == 0) {
s->db->updateEntriesFlag(1);
}
getHeadlines(AllArticles, true, !s->getSyncRead(), offset, FETCHER_SLOT(finishedStream));
}
void TTRssFetcher::finishedStream()
{
if (!processResponse()) {
return;
}
startJob(StoreStream);
}
void TTRssFetcher::finishedStream2()
{
Settings *s = Settings::instance();
if ((s->getRetentionDays() > 0 && lastDate > s->getRetentionDays()) ||
lastCount < streamLimit) {
offset = 0;
lastDate = s->getRetentionDays();
} else {
offset += lastCount;
commandList.prepend(currentCommand);
}
callNextCmd();
}
void TTRssFetcher::fetchStarredStream()
{
getHeadlines(Starred, true, false, offset, FETCHER_SLOT(finishedStream));
}
void TTRssFetcher::fetchPublishedStream()
{
getHeadlines(Published, true, false, offset, FETCHER_SLOT(finishedStream));
}
void TTRssFetcher::pruneOld()
{
Settings::instance()->db->removeEntriesByFlag(1);
callNextCmd();
}
void TTRssFetcher::startJob(Job job)
{
if (isRunning()) {
qWarning() << "Job is running";
return;
}
disconnect(this, SIGNAL(finished()), 0, 0);
currentJob = job;
switch (job) {
case StoreCategories:
case StoreFeeds:
connect(this, SIGNAL(finished()), this, SLOT(callNextCmd()));
break;
case StoreStream:
connect(this, SIGNAL(finished()), this, SLOT(finishedStream2()));
break;
default:
qWarning() << "Unknown Job!";
emit error(502);
setBusy(false);
return;
}
start(QThread::LowPriority);
}
void TTRssFetcher::run()
{
switch (currentJob) {
case StoreCategories:
storeCategories();
break;
case StoreFeeds:
storeFeeds();
break;
case StoreStream:
storeStream();
break;
default:
qWarning() << "Unknown Job!";
break;
}
}
void TTRssFetcher::storeCategories()
{
Settings *s = Settings::instance();
QString dashboardId = "ttrss";
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
if (jsonObj["content"].isArray()) {
QJsonArray arr = jsonObj["content"].toArray();
int end = arr.count();
#else
if (jsonObj["content"].type()==QVariant::List) {
QVariantList::const_iterator i = jsonObj["content"].toList().constBegin();
QVariantList::const_iterator end = jsonObj["content"].toList().constEnd();
#endif
for (int i = 0; i < end; ++i) {
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
QJsonObject obj = arr.at(i).toObject();
#else
QVariantMap obj = (*i).toMap();
#endif
if (obj["id"].toInt() >= 0) {
DatabaseManager::Tab t;
t.id = QString::number(obj["id"].toInt());
t.dashboardId = dashboardId;
t.title = obj["title"].toString();
s->db->writeTab(t);
}
}
} else {
qWarning() << "No categories found!";
}
}
void TTRssFetcher::storeFeeds()
{
Settings *s = Settings::instance();
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
if (jsonObj["content"].isArray()) {
QJsonArray arr = jsonObj["content"].toArray();
int end = arr.count();
#else
if (jsonObj["content"].type()==QVariant::List) {
QVariantList::const_iterator i = jsonObj["content"].toList().constBegin();
QVariantList::const_iterator end = jsonObj["content"].toList().constEnd();
#endif
for (int i = 0; i < end; ++i) {
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
QJsonObject obj = arr.at(i).toObject();
#else
QVariantMap obj = (*i).toMap();
#endif
DatabaseManager::Stream st;
st.id = QString::number(obj["id"].toInt());
st.title = obj["title"].toString();
st.link = obj["feed_url"].toString();
st.query = st.link;
st.content = "";
st.type = "";
st.unread = 0;
st.saved = 0;
st.read = 0;
st.slow = 0;
st.newestItemAddedAt = obj["last_updated"].toInt();
st.updateAt = st.newestItemAddedAt;
st.lastUpdate = QDateTime::currentDateTimeUtc().toTime_t();
if (obj["has_icon"].toBool()) {
st.icon = s->getUrl() + "/" + iconsUrl + "/" + st.id + ".ico";
DatabaseManager::CacheItem item;
item.origUrl = st.icon;
item.finalUrl = st.icon;
item.type = "icon";
emit addDownload(item);
}
s->db->writeStream(st);
DatabaseManager::Module m;
m.id = st.id;
m.name = st.title;
m.title = st.title;
m.status = "";
m.widgetId = "";
m.pageId = "";
m.tabId = QString::number(obj["cat_id"].toInt());
m.streamList.append(st.id);
s->db->writeModule(m);
}
} else {
qWarning() << "No feeds found!";
}
}
void TTRssFetcher::storeStream()
{
Settings *s = Settings::instance();
int count = 0;
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
if (jsonObj["content"].isArray()) {
QJsonArray arr = jsonObj["content"].toArray();
int end = arr.count();
count = end;
#else
if (jsonObj["content"].type()==QVariant::List) {
QVariantList::const_iterator i = jsonObj["content"].toList().constBegin();
QVariantList::const_iterator end = jsonObj["content"].toList().constEnd();
#endif
for (int i = 0; i < arr.count(); ++i) {
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
QJsonObject obj = arr.at(i).toObject();
#else
QVariantMap obj = (*i).toMap();
++count;
#endif
DatabaseManager::Entry e;
e.id = QString::number(obj["id"].toInt());
e.streamId = QString::number(obj["feed_id"].toInt());
e.title =obj["title"].toString();
e.author = obj["author"].toString();
if (obj["content"].isString())
e.content = obj["content"].toString();
e.link = obj["link"].toString();
e.read = obj["unread"].toBool() ? 0 : 1;
e.saved = obj["marked"].toBool() ? 1 : 0;
e.publishedAt = obj["updated"].toInt();
e.createdAt = obj["updated"].toInt();
e.cached = 0;
e.fresh = 1;
e.broadcast = obj["published"].toBool() ? 1 : 0;
e.timestamp = obj["updated"].toInt();
QRegExp rx("<img\\s[^>]*src\\s*=\\s*(\"[^\"]*\"|'[^']*')", Qt::CaseInsensitive);
if (rx.indexIn(e.content)!=-1) {
QString imgSrc = rx.cap(1); imgSrc = imgSrc.mid(1,imgSrc.length()-2);
if (imgSrc!="") {
if (s->getCachingMode() == 2 || (s->getCachingMode() == 1 && s->dm->isWLANConnected())) {
if (!s->db->isCacheExistsByFinalUrl(Utils::hash(imgSrc))) {
DatabaseManager::CacheItem item;
item.origUrl = imgSrc;
item.finalUrl = imgSrc;
item.type = "entry-image";
emit addDownload(item);
}
}
e.image = imgSrc;
}
}
s->db->writeEntry(e);
if (!e.saved && !e.broadcast && s->getRetentionDays() > 0) {
int date = QDateTime::fromTime_t(e.timestamp).daysTo(QDateTime::currentDateTimeUtc());
if (date > lastDate)
lastDate = date;
}
}
lastCount = count;
}
}
void TTRssFetcher::uploadActions()
{
if (!actionsList.isEmpty()) {
emit uploading();
setAction();
}
}
void TTRssFetcher::setAction()
{
Settings *s = Settings::instance();
DatabaseManager::Action action = actionsList.first();
QString ids;
int mode, field;
switch (action.type)
{
case DatabaseManager::SetRead:
case DatabaseManager::UnSetRead:
{
ids = action.id1.replace('&', ',');
mode = action.type == DatabaseManager::SetRead ? 0 : 1;
field = 2;
break;
}
case DatabaseManager::SetSaved:
case DatabaseManager::UnSetSaved:
{
ids = action.id1.replace('&', ',');
mode = action.type == DatabaseManager::SetSaved ? 1 : 0;
field = 0;
break;
}
case DatabaseManager::SetBroadcast:
case DatabaseManager::UnSetBroadcast:
{
ids = action.id1.replace('&', ',');
mode = action.type == DatabaseManager::SetBroadcast ? 1 : 0;
field = 1;
break;
}
case DatabaseManager::SetStreamReadAll:
case DatabaseManager::UnSetStreamReadAll:
{
ids = mergeEntryIds(s->db->readEntriesByStream(action.id1, 0, s->db->countEntriesByStream(action.id1)),
action.type == DatabaseManager::SetStreamReadAll);
mode = action.type == DatabaseManager::SetStreamReadAll ? 0 : 1;
field = 2;
break;
}
case DatabaseManager::SetTabReadAll:
case DatabaseManager::UnSetTabReadAll:
{
QList<QString> streams = s->db->readStreamIdsByTab(action.id1);
for (int i = 0; i < streams.count(); ++i) {
if (!ids.isEmpty())
ids += ",";
ids += mergeEntryIds(s->db->readEntriesByStream(streams[i], 0, s->db->countEntriesByStream(streams[i])),
action.type == DatabaseManager::SetTabReadAll);
}
mode = action.type == DatabaseManager::SetTabReadAll ? 0 : 1;
field = 2;
break;
}
case DatabaseManager::SetAllRead:
case DatabaseManager::UnSetAllRead:
{
QList<DatabaseManager::Stream> streams = s->db->readStreamsByDashboard(action.id1);
for (int i = 0; i < streams.count(); ++i) {
if (!ids.isEmpty())
ids += ",";
ids += mergeEntryIds(s->db->readEntriesByStream(streams[i].id, 0, s->db->countEntriesByStream(streams[i].id)),
action.type == DatabaseManager::SetAllRead);
}
mode = action.type == DatabaseManager::SetAllRead ? 0 : 1;
field = 2;
break;
}
case DatabaseManager::SetListRead:
case DatabaseManager::UnSetListRead:
{
ids = action.id1.replace('&', ',');
mode = action.type == DatabaseManager::SetListRead ? 0 : 1;
field = 2;
break;
}
case DatabaseManager::SetListSaved:
case DatabaseManager::UnSetListSaved:
{
ids = action.id1.replace('&', ',');
mode = action.type == DatabaseManager::SetListSaved ? 1 : 0;
field = 1;
break;
}
default:
qWarning() << "Unknown action type: " << action.type;
finishedSetAction();
return;
}
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
QJsonObject params;
params["article_ids"] = ids;
params["mode"] = mode;
params["field"] = field;
#else
QString params = "\"article_ids\":\"" + ids + "\",\"mode\":" + QString::number(mode) +
",\"field\":" + QString::number(field);
#endif
sendApiCall("updateArticle", params, FETCHER_SLOT(finishedSetAction));
}
void TTRssFetcher::finishedSetAction()
{
if (!processResponse()) {
return;
}
Settings *s = Settings::instance();
DatabaseManager::Action action = actionsList.takeFirst();
s->db->removeActionsByIdAndType(action.id1, action.type);
emit uploadProgress(uploadProggressTotal - actionsList.size(), uploadProggressTotal);
if (actionsList.isEmpty()) {
startFetching();
} else {
setAction();
}
}
void TTRssFetcher::callNextCmd()
{
if (!commandList.isEmpty()) {
proggress = proggressTotal - commandList.size() - Settings::instance()->getRetentionDays() + lastDate;
emit progress(proggress, proggressTotal);
currentCommand = commandList.takeFirst();
(this->*currentCommand)();
} else {
taskEnd();
}
}
QString TTRssFetcher::mergeEntryIds(const QList<DatabaseManager::Entry>& entries, bool read)
{
QString ids;
for (int i = 0; i < entries.count(); ++i) {
if (entries[i].read == read) {
if (!ids.isEmpty())
ids += ",";
ids += entries[i].id;
}
}
return ids;
}
void TTRssFetcher::getHeadlines(int feedId, bool getContent, bool unreadOnly, int offset, ReplyCallback callback)
{
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
QJsonObject params;
params["feed_id"] = feedId;
params["show_content"] = getContent;
params["view_mode"] = unreadOnly ? "unread" : "all_articles";
params["include_attachments"] = getContent;
params["order_by"] = "feed_dates";
params["skip"] = offset;
params["limit"] = streamLimit;
#else
QString params = "\"feed_id\":" + QString::number(feedId) + "," +
"\"show_content\":" + (getContent ? "true," : "false,") +
"\"show_excerpt\":false," +
"\"view_mode\":" + (unreadOnly ? "\"unread\"," : "\"all_articles\",") +
"\"include_attachments\":" + (getContent ? "true," : "false,") +
"\"order_by\":\"feed_dates\"," +
"\"skip\":" + QString::number(offset) + "," +
"\"limit\":" + QString::number(streamLimit);
#endif
sendApiCall("getHeadlines", params, callback);
}
void TTRssFetcher::sendApiCall(const QString& op, ReplyCallback callback)
{
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
QJsonObject params;
#else
QString params;
#endif
sendApiCall(op, params, callback);
}
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
void TTRssFetcher::sendApiCall(const QString& op, const QJsonObject& params, ReplyCallback callback)
#else
void TTRssFetcher::sendApiCall(const QString& op, const QString& params, ReplyCallback callback);
#endif
{
data.clear();
if (currentReply != NULL) {
currentReply->disconnect();
currentReply->deleteLater();
currentReply = NULL;
}
QNetworkRequest request(QUrl(Settings::instance()->getUrl() + "/api/"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json; charset=UTF-8");
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
QJsonObject obj (params);
if (!sessionId.isEmpty()) {
obj["sid"] = sessionId;
}
obj["op"] = op;
QString body = QJsonDocument(obj).toJson(QJsonDocument::Compact);
#else
QString body = "{\"op\":\"" + op + "\"" + (!params.isEmpty() ? params : "") + "}";
#endif
currentReply = nam.post(request, body.toUtf8());
connect(currentReply, &QNetworkReply::finished, this, callback);
connect(currentReply, SIGNAL(readyRead()), this, SLOT(readyRead()));
connect(currentReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(networkError(QNetworkReply::NetworkError)));
}
bool TTRssFetcher::processResponse()
{
if (currentReply->error() && currentReply->error() != QNetworkReply::OperationCanceledError) {
emit error(500);
setBusy(false);
return false;
}
if (!parse()) {
qWarning() << "Error parsing Json!";
emit error(600);
setBusy(false);
return false;
} else if (jsonObj["status"].toInt() != 0) {
QString err;
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
err = jsonObj["content"].toObject()["error"].toString();
#else
err = jsonObj["content"].toMap()["error"].toString();
#endif
qWarning() << "Error: " << err;
if (err == "LOGIN_ERROR") {
if (busyType == Fetcher::CheckingCredentials) {
emit errorCheckingCredentials(501);
} else {
emit error(402);
}
} else if (err == "NOT_LOGGED_IN") {
emit error(401);
} else {
emit error(601);
}
setBusy(false);
return false;
}
return true;
}

134
sailfish/src/ttrssfetcher.h Normal file
View file

@ -0,0 +1,134 @@
/*
Copyright (C) 2019 Michal Kosciesza <michal@mkiol.net>
Copyright (C) 2019 Renaud Casenave-Péré <renaud@casenave-pere.fr>
This file is part of Kaktus.
Kaktus is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Kaktus is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Kaktus. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TTRSSFETCHER_H
#define TTRSSFETCHER_H
#include <QObject>
#include <QString>
#include <QStringList>
#include <QList>
#include <QVariantMap>
#include <QNetworkRequest>
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
#include <QJsonArray>
#else
#include <QVariantList>
#endif
#include "fetcher.h"
#include "databasemanager.h"
class TTRssFetcher : public Fetcher
{
Q_OBJECT
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
typedef void (TTRssFetcher::*ReplyCallback)();
#else
typedef QString ReplyCallback;
#endif
typedef void (TTRssFetcher::*ChainCommand)();
enum Job {
Idle,
StoreCategories,
StoreFeeds,
StoreStream
};
enum SpecialFeed {
AllArticles = -4,
Fresh = -3,
Published = -2,
Starred = -1
};
public:
explicit TTRssFetcher(QObject *parent = 0);
virtual ~TTRssFetcher();
Q_INVOKABLE virtual void getConnectUrl(int) {}
Q_INVOKABLE virtual bool setConnectUrl(const QString &) { return true; }
protected:
void run();
private Q_SLOTS:
void finishedSignIn();
void finishedConfig();
void finishedCategories();
void finishedFeeds();
void finishedStream();
void finishedStream2();
void finishedSetAction();
void callNextCmd();
private:
virtual void signIn();
virtual void startFetching();
virtual void uploadActions();
void fetchCategories();
void fetchFeeds();
void fetchStream();
void fetchStarredStream();
void fetchPublishedStream();
void pruneOld();
void setAction();
void startJob(Job job);
void storeCategories();
void storeFeeds();
void storeStream();
QString mergeEntryIds(const QList<DatabaseManager::Entry>& entries, bool read);
void getHeadlines(int feedId, bool getContent, bool unreadOnly, int offset, ReplyCallback callback);
void sendApiCall(const QString& op, ReplyCallback callback);
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
void sendApiCall(const QString& op, const QJsonObject& params, ReplyCallback callback);
#else
void sendApiCall(const QString& op, const QString& params, ReplyCallback callback);
#endif
bool processResponse();
private:
static const int streamLimit = 100;
QString sessionId;
int apiLevel;
QString iconsUrl;
QList<int> processedActionList;
QList<ChainCommand> commandList;
ChainCommand currentCommand;
Job currentJob;
int lastDate;
int lastCount;
int offset;
};
#endif // TTRSSFETCHER_H

View file

@ -68,6 +68,7 @@
#include "oldreaderfetcher.h"
#include "nvfetcher.h"
#include "feedlyfetcher.h"
#include "ttrssfetcher.h"
Utils::Utils(QObject *parent) :
QObject(parent)//, ncm(new QNetworkConfigurationManager(parent))
@ -813,6 +814,11 @@ void Utils::resetFetcher(int type)
s->fetcher = new FeedlyFetcher();
}
if (type == 4) {
// Tiny Tiny Rss fetcher
s->fetcher = new TTRssFetcher();
}
if (s->fetcher != NULL)
#ifdef BB10
s->qml->setContextProperty("fetcher", s->fetcher);