Worked on Nextcloud API capabilities (not tested\!)

This commit is contained in:
Scharel 2021-04-12 18:58:39 +02:00
parent 7e1cc5cf1e
commit e44af7ec7a
15 changed files with 164 additions and 73 deletions

View file

@ -30,13 +30,10 @@ ApplicationWindow
}
onSortByChanged: {
if (sortBy == "none")
notesProxyModel.invalidate()
else
notesProxyModel.sortRole = notesModel.roleFromName(sortBy)
if (sortBy == "none") notesModel.invalidate()
}
onFavoritesOnTopChanged: {
notesProxyModel.favoritesOnTop = favoritesOnTop
notesModel.favoritesOnTop = favoritesOnTop
}
function createAccount(username, password, url, name) {
@ -138,7 +135,7 @@ ApplicationWindow
}
Nextcloud {
id: nextcloud
id: notesApi
server: account.url
username: account.username
password: account.passowrd
@ -155,5 +152,6 @@ ApplicationWindow
initialPage: Component { NotesPage { } }
cover: Qt.resolvedUrl("cover/CoverPage.qml")
allowedOrientations: defaultAllowedOrientations
allowedOrientations: debug ? Orientation.All : defaultAllowedOrientations
}

View file

@ -89,5 +89,5 @@ Page {
VerticalScrollDecorator {}
}
allowedOrientations: defaultAllowedOrientations
allowedOrientations: appWindow.allowedOrientations
}

View file

@ -179,5 +179,5 @@ Dialog {
VerticalScrollDecorator {}
}
allowedOrientations: defaultAllowedOrientations
allowedOrientations: appWindow.allowedOrientations
}

View file

@ -111,5 +111,5 @@ NO WARRANTY
}
}
allowedOrientations: defaultAllowedOrientations
allowedOrientations: appWindow.allowedOrientations
}

View file

@ -1,7 +1,7 @@
import QtQuick 2.2
import Sailfish.Silica 1.0
import Nemo.Configuration 1.0
import NextcloudNotes 1.0
import NextcloudApi 1.0
Dialog {
id: loginDialog
@ -308,5 +308,5 @@ Dialog {
}
}
allowedOrientations: defaultAllowedOrientations
allowedOrientations: appWindow.allowedOrientations
}

View file

@ -44,5 +44,5 @@ SOFTWARE.</p>"
}
}
allowedOrientations: defaultAllowedOrientations
allowedOrientations: appWindow.allowedOrientations
}

View file

@ -261,5 +261,5 @@ Dialog {
VerticalScrollDecorator {}
}
allowedOrientations: defaultAllowedOrientations
allowedOrientations: appWindow.allowedOrientations
}

View file

@ -38,7 +38,7 @@ Page {
MenuItem {
text: notesApi.networkAccessible && !notesApi.busy ? qsTr("Reload") : qsTr("Updating...")
enabled: account !== null && notesApi.networkAccessible && !notesApi.busy
onClicked: notes.getAllNotes()
onClicked: notesApi.getAllNotes()
}
MenuLabel {
visible: account !== null
@ -59,7 +59,7 @@ Page {
EnterKey.iconSource: "image://theme/icon-m-enter-close"
EnterKey.onClicked: focus = false
onTextChanged: {
notesProxyModel.searchFilter = text
notesModel.searchFilter = text
}
}
Label {
@ -84,7 +84,7 @@ Page {
currentIndex: -1
model: notesProxyModel
model: notesModel
delegate: BackgroundItem {
id: note
@ -238,7 +238,7 @@ Page {
ViewPlaceholder {
id: noSearchPlaceholder
enabled: notesList.count === 0 && notesProxyModel.searchFilter !== "" //notesProxyModel.filterRegExp !== ""
enabled: notesList.count === 0 && notesModel.searchFilter !== "" //notesModel.filterRegExp !== ""
text: qsTr("No result")
hintText: qsTr("Try another query")
}
@ -266,5 +266,5 @@ Page {
VerticalScrollDecorator { flickable: notesList }
}
allowedOrientations: defaultAllowedOrientations
allowedOrientations: appWindow.allowedOrientations
}

View file

@ -231,5 +231,5 @@ Page {
VerticalScrollDecorator {}
}
allowedOrientations: defaultAllowedOrientations
allowedOrientations: appWindow.allowedOrientations
}

View file

@ -394,5 +394,5 @@ Use <b>HTML</b> <i>Tags</i> to format your text."
}
}
allowedOrientations: defaultAllowedOrientations
allowedOrientations: appWindow.allowedOrientations
}

View file

@ -39,5 +39,5 @@ Dialog {
}
}
allowedOrientations: defaultAllowedOrientations
allowedOrientations: appWindow.allowedOrientations
}

View file

@ -17,6 +17,7 @@ int main(int argc, char *argv[])
qDebug() << app->applicationDisplayName() << app->applicationVersion();
NotesProxyModel* notesProxyModel = new NotesProxyModel;
AccountHash* accountHash = new AccountHash;
qmlRegisterType<NextcloudApi>("NextcloudApi", 1, 0, "Nextcloud");
@ -26,6 +27,7 @@ int main(int argc, char *argv[])
#else
view->rootContext()->setContextProperty("debug", QVariant(false));
#endif
view->rootContext()->setContextProperty("notesModel", notesProxyModel);
view->rootContext()->setContextProperty("accountHash", accountHash);
view->setSource(SailfishApp::pathTo("qml/harbour-nextcloudnotes.qml"));

View file

@ -15,6 +15,9 @@ NextcloudApi::NextcloudApi(QObject *parent) : QObject(parent)
m_status_needsDbUpgrade = false;
m_status_extendedSupport = false;
// Add capabilities functions for server apps
m_appCapabilities["notes"] = &NextcloudApi::updateNoteCapabilities;
// Login Flow V2 poll timer
m_loginPollTimer.setInterval(LOGIN_FLOWV2_POLL_INTERVALL);
connect(&m_loginPollTimer, SIGNAL(timeout()), this, SLOT(pollLoginUrl()));
@ -118,6 +121,15 @@ void NextcloudApi::setPort(int port) {
}
}
void NextcloudApi::setPath(QString path) {
if (path != m_url.path()) {
m_url.setPath(path);
emit pathChanged(m_url.path());
emit serverChanged(server());
emit urlChanged(m_url);
}
}
void NextcloudApi::setUsername(QString username) {
if (username != m_url.userName()) {
m_url.setUserName(username);
@ -142,13 +154,23 @@ void NextcloudApi::setPassword(QString password) {
}
}
void NextcloudApi::setPath(QString path) {
if (path != m_url.path()) {
m_url.setPath(path);
emit pathChanged(m_url.path());
emit serverChanged(server());
emit urlChanged(m_url);
bool NextcloudApi::notesAppInstalled() const {
QJsonObject notes = m_capabilities.value("notes").toObject();
return !notes.isEmpty();
}
QStringList NextcloudApi::notesAppApiVersions() const {
QStringList versions;
QJsonObject notes = m_capabilities.value("notes").toObject();
if (!notes.isEmpty()) {
QJsonArray apiVersion = notes.value("api_version").toArray();
QJsonArray::const_iterator i;
for (i = apiVersion.constBegin(); i != apiVersion.constEnd(); ++i) {
if (i->isString())
versions << i->toString();
}
}
return versions;
}
const QString NextcloudApi::errorMessage(int error) const {
@ -288,6 +310,18 @@ bool NextcloudApi::deleteAppPassword() {
return del(DEL_APPPASSWORD_ENDPOINT, true);
}
bool NextcloudApi::getUserList() {
return false; // TODO
}
bool NextcloudApi::getUserMetaData(const QString& user) {
return false; // TODO
}
bool NextcloudApi::getCapabilities() {
return false; // TODO
}
void NextcloudApi::verifyUrl(QUrl url) {
emit urlValidChanged(
url.isValid()&&
@ -608,9 +642,53 @@ void NextcloudApi::setUserMetaStatus(ApiCallStatus status, bool *changed) {
}
bool NextcloudApi::updateCapabilities(const QJsonObject &json) {
QJsonObject ocs = json.value("ocs").toObject();
QJsonObject data = ocs.value("data").toObject();
QJsonObject preCapabilities = m_capabilities;
QJsonObject newCapabilities = data.value("capabilities").toObject();
if (!newCapabilities.isEmpty()) {
setCababilitiesStatus(ApiSuccess);
if (newCapabilities != preCapabilities) {
m_capabilities = newCapabilities;
QStringList apps = newCapabilities.keys();
QStringList::const_iterator app;
for (app = apps.constBegin(); app != apps.constEnd(); ++app) {
if (m_appCapabilities.contains(*app)) {
qDebug() << "Updating \"" << *app << "\" capabilities";
(this->*m_appCapabilities[*app])(newCapabilities.value(*app).toObject(), preCapabilities.value(*app).toObject());
}
else {
qDebug() << "Capabilities for " << *app << " not implemented!";
}
}
}
return true;
}
setCababilitiesStatus(ApiFailed);
return false;
}
void NextcloudApi::updateNoteCapabilities(const QJsonObject &newObject, const QJsonObject &preObject) {
qDebug() << "Updating \"notes\" capabilities";
if (newObject.isEmpty() != preObject.isEmpty())
emit notesAppInstalledChanged(notesAppInstalled());
QStringList preVersions;
QJsonArray preApiVersion = preObject.value("api_version").toArray();
QJsonArray::const_iterator i;
for (i = preApiVersion.constBegin(); i != preApiVersion.constEnd(); ++i) {
if (i->isString())
preVersions << i->toString();
}
if (preVersions != notesAppApiVersions())
emit notesAppApiVersionsChanged(notesAppApiVersions());
}
void NextcloudApi::setCababilitiesStatus(ApiCallStatus status, bool *changed) {
if (status != m_capabilitiesStatus) {
m_capabilitiesStatus = status;
if (changed) *changed = true;
emit capabilitiesStatusChanged(m_capabilitiesStatus);
}
}

View file

@ -16,21 +16,24 @@
// Nextcloud instance information
const QString STATUS_ENDPOINT("/status.php");
// Capabilites and users
// OCS APIs endpoints
// https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html
const QString CAPABILITIES_ENDPOINT("/ocs/v2.php/cloud/capabilities");
const QString LIST_USERS_ENDPOINT("/ocs/v2.php/cloud/users");
const QString USER_METADATA_ENDPOINT("/ocs/v2.php/cloud/users/%1");
const QString USER_NOTIFICATION_ENDPOINT("/ocs/v2.php/cloud/capabilities");
const QString USER_NOTIFICATION_ENDPOINT("/ocs/v2.php/apps/notifications/api/v2/notifications");
const QString DIRECT_DOWNLOAD_ENDPOINT("/ocs/v2.php/apps/dav/api/v1/direct");
// Login and authentication
// Login Flow endpoints
// https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html
const QString GET_APPPASSWORD_ENDPOINT("/ocs/v2.php/core/getapppassword");
const QString DEL_APPPASSWORD_ENDPOINT("/ocs/v2.php/core/apppassword");
const QString LOGIN_FLOWV2_ENDPOINT("/index.php/login/v2");
const int LOGIN_FLOWV2_MIN_VERSION = 16;
const int LOGIN_FLOWV2_POLL_INTERVALL = 5000;
// Diredct Download
const QString DIRECT_DOWNLOAD_ENDPOINT("/ocs/v2.php/apps/dav/api/v1/direct");
class NextcloudApi;
typedef void (NextcloudApi::*updateAppCapabilities)(const QJsonObject &newObject, const QJsonObject &preObject);
class NextcloudApi : public QObject
{
@ -38,9 +41,9 @@ class NextcloudApi : public QObject
// Generic API properties
Q_PROPERTY(bool verifySsl READ verifySsl WRITE setVerifySsl NOTIFY verifySslChanged) // to allow selfsigned certificates
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) // complete API URL = <scheme>://<username>:<password>@<host>[:<port>]/<path>
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) // complete nextcloud URL = <scheme>://<username>:<password>@<host>[:<port>]/<path>
Q_PROPERTY(QString server READ server WRITE setServer NOTIFY serverChanged) // url without username and password = <scheme>://<host>[:<port>]/<path>
// the following six properties will update the url and server properties and vice versa
// the following properties will update the url and server properties and vice versa
Q_PROPERTY(QString scheme READ scheme WRITE setScheme NOTIFY schemeChanged)
Q_PROPERTY(QString host READ host WRITE setHost NOTIFY hostChanged)
Q_PROPERTY(int port READ port WRITE setPort NOTIFY portChanged)
@ -49,10 +52,10 @@ class NextcloudApi : public QObject
Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged)
// Networking status information
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
Q_PROPERTY(bool urlValid READ urlValid NOTIFY urlValidChanged)
Q_PROPERTY(bool networkAccessible READ networkAccessible NOTIFY networkAccessibleChanged)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) // when all needed properties are set
Q_PROPERTY(bool urlValid READ urlValid NOTIFY urlValidChanged) // if the property url is valid
Q_PROPERTY(bool networkAccessible READ networkAccessible NOTIFY networkAccessibleChanged) // when the device has connectivity
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) // when an API call is currently running
// Nextcloud status (status.php), these properties will be automatically updated on changes of the generic properties
Q_PROPERTY(ApiCallStatus statusStatus READ statusStatus NOTIFY statusStatusChanged)
@ -66,17 +69,21 @@ class NextcloudApi : public QObject
Q_PROPERTY(bool statusExtendedSupport READ statusExtendedSupport NOTIFY statusExtendedSupportChanged)
// Login status
Q_PROPERTY(LoginStatus loginStatus READ loginStatus NOTIFY loginStatusChanged)
Q_PROPERTY(bool loginFlowV2Possible READ loginFlowV2Possible NOTIFY loginFlowV2PossibleChanged)
Q_PROPERTY(QUrl loginUrl READ loginUrl NOTIFY loginUrlChanged)
Q_PROPERTY(ApiCallStatus loginStatus READ loginStatus NOTIFY loginStatusChanged)
Q_PROPERTY(QUrl loginUrl READ loginUrl NOTIFY loginUrlChanged) // will be set after initiateFlowV2Login() has been called. The URL needs to be opened in a browser or webview
// User(s) status
Q_PROPERTY(ApiCallStatus userListStatus READ userListStatus NOTIFY userListStatusChanged)
Q_PROPERTY(QStringList userList READ userList NOTIFY userListChanged)
Q_PROPERTY(ApiCallStatus userMetaStatus READ userMetaStatus NOTIFY userMetaStatusChanged)
Q_PROPERTY(QStringList userList READ userList NOTIFY userListChanged)
// TODO property for user metadata
// Nextcloud capabilities
Q_PROPERTY(ApiCallStatus capabilitiesStatus READ capabilitiesStatus NOTIFY capabilitiesStatusChanged)
Q_PROPERTY(bool notesAppInstalled READ notesAppInstalled NOTIFY notesAppInstalledChanged)
Q_PROPERTY(QStringList notesAppApiVersions READ notesAppApiVersions NOTIFY notesAppApiVersionsChanged)
// TODO other property for server capabilities
public:
explicit NextcloudApi(QObject *parent = nullptr);
@ -148,17 +155,19 @@ public:
bool statusExtendedSupport() const { return m_status_extendedSupport; }
// Login status
LoginStatus loginStatus() const { return m_loginStatus; }
bool loginFlowV2Possible() const { return QVersionNumber::fromString(statusVersion()) >= QVersionNumber(LOGIN_FLOWV2_MIN_VERSION); }
QUrl loginUrl() const { return m_loginUrl; }
LoginStatus loginStatus() const { return m_loginStatus; }
// User(s) status
ApiCallStatus userListStatus() const { return m_userListStatus; }
QStringList userList() const { return m_userList; }
ApiCallStatus userMetaStatus() const { return m_userMetaStatus; }
QStringList userList() const { return m_userList; }
// Nextcloud capabilities
ApiCallStatus capabilitiesStatus() const { return m_capabilitiesStatus; }
bool notesAppInstalled() const;
QStringList notesAppApiVersions() const;
enum ErrorCodes {
NoError,
@ -229,6 +238,8 @@ signals:
// Nextcloud capabilities
void capabilitiesStatusChanged(ApiCallStatus status);
void notesAppInstalledChanged(bool installed);
void notesAppApiVersionsChanged(QStringList versions);
// API helper updates
void getFinished(QNetworkReply* reply);
@ -265,11 +276,18 @@ private:
QString m_status_productname;
bool m_status_extendedSupport;
// Nextcloud capabilities
bool updateCapabilities(const QJsonObject &json);
void setCababilitiesStatus(ApiCallStatus status, bool *changed = NULL);
ApiCallStatus m_capabilitiesStatus;
QJsonObject m_capabilities;
// Nextcloud Login Flow v2
// https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
bool updateLoginFlow(const QJsonObject &json);
bool updateLoginCredentials(const QJsonObject &json);
bool updateAppPassword(const QJsonObject &json);
bool deleteAppPassword(const QJsonObject &json);
void setLoginStatus(LoginStatus status, bool *changed = NULL);
LoginStatus m_loginStatus;
QTimer m_loginPollTimer;
QUrl m_loginUrl;
QUrl m_pollUrl;
QString m_pollToken;
// Nextcloud users list
bool updateUserList(const QJsonObject &json);
@ -283,18 +301,13 @@ private:
ApiCallStatus m_userMetaStatus;
QHash<QString, QJsonObject> m_userMeta;
// Nextcloud Login Flow v2
// https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
bool updateLoginFlow(const QJsonObject &json);
bool updateLoginCredentials(const QJsonObject &json);
bool updateAppPassword(const QJsonObject &json);
bool deleteAppPassword(const QJsonObject &json);
void setLoginStatus(LoginStatus status, bool *changed = NULL);
LoginStatus m_loginStatus;
QTimer m_loginPollTimer;
QUrl m_loginUrl;
QUrl m_pollUrl;
QString m_pollToken;
// Nextcloud capabilities
bool updateCapabilities(const QJsonObject &json);
QMap<QString, updateAppCapabilities> m_appCapabilities;
void updateNoteCapabilities(const QJsonObject &newObject, const QJsonObject &preObject = QJsonObject());
void setCababilitiesStatus(ApiCallStatus status, bool *changed = NULL);
ApiCallStatus m_capabilitiesStatus;
QJsonObject m_capabilities;
};
#endif // NEXTCLOUDAPI_H

View file

@ -275,32 +275,32 @@
<context>
<name>NextcloudApi</name>
<message>
<location filename="../src/nextcloudapi.cpp" line="157"/>
<location filename="../src/nextcloudapi.cpp" line="158"/>
<source>No error</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/nextcloudapi.cpp" line="160"/>
<location filename="../src/nextcloudapi.cpp" line="161"/>
<source>No network connection available</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/nextcloudapi.cpp" line="163"/>
<location filename="../src/nextcloudapi.cpp" line="164"/>
<source>Failed to communicate with the Nextcloud server</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/nextcloudapi.cpp" line="166"/>
<location filename="../src/nextcloudapi.cpp" line="167"/>
<source>An error occured while establishing an encrypted connection</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/nextcloudapi.cpp" line="169"/>
<location filename="../src/nextcloudapi.cpp" line="170"/>
<source>Could not authenticate to the Nextcloud instance</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../src/nextcloudapi.cpp" line="172"/>
<location filename="../src/nextcloudapi.cpp" line="173"/>
<source>Unknown error</source>
<translation type="unfinished"></translation>
</message>
@ -912,27 +912,27 @@ You can also use other markdown syntax inside them.</source>
<context>
<name>harbour-nextcloudnotes</name>
<message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="101"/>
<location filename="../qml/harbour-nextcloudnotes.qml" line="98"/>
<source>Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="102"/>
<location filename="../qml/harbour-nextcloudnotes.qml" line="99"/>
<source>Offline</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="103"/>
<location filename="../qml/harbour-nextcloudnotes.qml" line="100"/>
<source>Synced</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="117"/>
<location filename="../qml/harbour-nextcloudnotes.qml" line="114"/>
<source>API error</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../qml/harbour-nextcloudnotes.qml" line="110"/>
<location filename="../qml/harbour-nextcloudnotes.qml" line="107"/>
<source>File error</source>
<translation type="unfinished"></translation>
</message>