From 81502588146f9944a897ec8273d9b1d59681abf3 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Wed, 17 Feb 2021 20:59:45 +0100 Subject: [PATCH] added Tempo plugin --- debian/control | 9 ++ debian/nymea-plugin-tempo.install.in | 1 + nymea-plugins.pro | 1 + tempo/README.md | 17 +++ tempo/integrationplugintempo.cpp | 181 +++++++++++++++++++++++++++ tempo/integrationplugintempo.h | 68 ++++++++++ tempo/integrationplugintempo.json | 172 +++++++++++++++++++++++++ tempo/meta.json | 0 tempo/tempo.cpp | 155 +++++++++++++++++++++++ tempo/tempo.h | 135 ++++++++++++++++++++ tempo/tempo.png | Bin 0 -> 29508 bytes tempo/tempo.pro | 14 +++ 12 files changed, 753 insertions(+) create mode 100644 debian/nymea-plugin-tempo.install.in create mode 100644 tempo/README.md create mode 100644 tempo/integrationplugintempo.cpp create mode 100644 tempo/integrationplugintempo.h create mode 100644 tempo/integrationplugintempo.json create mode 100644 tempo/meta.json create mode 100644 tempo/tempo.cpp create mode 100644 tempo/tempo.h create mode 100644 tempo/tempo.png create mode 100644 tempo/tempo.pro diff --git a/debian/control b/debian/control index db1cc724..03b23488 100644 --- a/debian/control +++ b/debian/control @@ -700,6 +700,15 @@ Depends: ${shlibs:Depends}, nymea-plugins-translations, Description: nymea.io plugin for Telegram This plugin allows nymea to send messages to telegram via the bot API. + + +Package: nymea-plugin-tempo +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-translations, +Description: nymea.io plugin for Tempo time tracking + This package will install the nymea.io plugin for Tempo time tracking. Package: nymea-plugin-tplink diff --git a/debian/nymea-plugin-tempo.install.in b/debian/nymea-plugin-tempo.install.in new file mode 100644 index 00000000..c63e05ef --- /dev/null +++ b/debian/nymea-plugin-tempo.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginstempo.so diff --git a/nymea-plugins.pro b/nymea-plugins.pro index eb037d01..a92e5782 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -58,6 +58,7 @@ PLUGIN_DIRS = \ tasmota \ tcpcommander \ telegram \ + tempo \ texasinstruments \ tplink \ tuya \ diff --git a/tempo/README.md b/tempo/README.md new file mode 100644 index 00000000..cc12441a --- /dev/null +++ b/tempo/README.md @@ -0,0 +1,17 @@ +# Tempo + +Add Tempo time tracking accounts to nymea. + +## Supported Things + +* Tempo Atlassian Account + * Time Accounts + +## Requirements + +* Package 'nymea-plugin-tempo' must be installed +* A atlassian account and client credentials + +## More + +https://apidocs.tempo.io/ diff --git a/tempo/integrationplugintempo.cpp b/tempo/integrationplugintempo.cpp new file mode 100644 index 00000000..9cc92cb4 --- /dev/null +++ b/tempo/integrationplugintempo.cpp @@ -0,0 +1,181 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "integrationplugintempo.h" +#include "plugininfo.h" + +#include "network/networkaccessmanager.h" + +#include +#include +#include +#include +#include + +IntegrationPluginTempo::IntegrationPluginTempo() +{ +} + +void IntegrationPluginTempo::startPairing(ThingPairingInfo *info) +{ + qCDebug(dcTempo()) << "Start pairing"; + + if (info->thingClassId() == tempoConnectionThingClassId) { + + QByteArray clientId = configValue(tempoPluginCustomClientIdParamTypeId).toByteArray(); + QByteArray clientSecret = configValue(tempoPluginCustomClientSecretParamTypeId).toByteArray(); + if (clientId.isEmpty() || clientSecret.isEmpty()) { + clientId = apiKeyStorage()->requestKey("tempo").data("clientId"); + clientSecret = apiKeyStorage()->requestKey("tempo").data("clientSecret"); + } else { + qCDebug(dcTempo()) << "Using custom client secret and id"; + } + if (clientId.isEmpty() || clientSecret.isEmpty()) { + info->finish(Thing::ThingErrorAuthenticationFailure, tr("Client id and/or seceret is not available.")); + return; + } + + QString jiraCloudInstanceName = info->params().paramValue(tempoConnectionAtlassianAccountParamTypeId).toString(); + QUrl url = Tempo::getLoginUrl(QUrl("https://127.0.0.1:8888"), jiraCloudInstanceName, clientId); + qCDebug(dcTempo()) << "Checking if the Tempo server is reachable"; + QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url)); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, info, [reply, info, url, this] { + + if (reply->error() != QNetworkReply::NetworkError::HostNotFoundError) { + qCDebug(dcTempo()) << "Tempo server is reachable"; + info->setOAuthUrl(url); + info->finish(Thing::ThingErrorNoError); + } else { + qCWarning(dcTempo()) << "Got online check error" << reply->error() << reply->errorString(); + info->finish(Thing::ThingErrorSetupFailed, tr("Tempo server not reachable, please check the internet connection")); + } + }); + } else { + qCWarning(dcTempo()) << "Unhandled pairing method!"; + info->finish(Thing::ThingErrorCreationMethodNotSupported); + } +} + +void IntegrationPluginTempo::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) +{ + Q_UNUSED(username); + qCDebug(dcTempo()) << "Confirm pairing"; + if (info->thingClassId() == tempoConnectionThingClassId) { + + QUrl url(secret); + QUrlQuery query(url); + QByteArray authorizationCode = query.queryItemValue("code").toLocal8Bit(); + if (authorizationCode.isEmpty()) { + qCWarning(dcTempo()) << "No authorization code received."; + return info->finish(Thing::ThingErrorAuthenticationFailure); + } + + Tempo *tempo = m_setupTempoConnections.value(info->thingId()); + if (!tempo) { + qWarning(dcTempo()) << "No Tempo connection found for device:" << info->thingName(); + m_setupTempoConnections.remove(info->thingId()); + return info->finish(Thing::ThingErrorHardwareFailure); + } + qCDebug(dcTempo()) << "Authorization code" << authorizationCode.mid(0, 4)+QString().fill('*', authorizationCode.length()-4) ; + tempo->getAccessTokenFromAuthorizationCode(authorizationCode); + connect(tempo, &Tempo::receivedRefreshToken, info, [info, this](const QByteArray &refreshToken){ + qCDebug(dcTempo()) << "Token:" << refreshToken.mid(0, 4)+QString().fill('*', refreshToken.length()-4) ; + + pluginStorage()->beginGroup(info->thingId().toString()); + pluginStorage()->setValue("refresh_token", refreshToken); + pluginStorage()->endGroup(); + + info->finish(Thing::ThingErrorNoError); + }); + } else { + Q_ASSERT_X(false, "confirmPairing", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginTempo::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcTempo()) << "Setup thing"; + + if (thing->thingClassId() == tempoConnectionThingClassId) { + + } +} + +void IntegrationPluginTempo::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + Action action = info->action(); + + if (thing->thingClassId() == tempoConnectionThingClassId) { + + } else if (thing->thingClassId() == accountThingClassId) { + + } +} + +void IntegrationPluginTempo::thingRemoved(Thing *thing) +{ + qCDebug(dcTempo()) << "Thing removed" << thing->name(); +} + +void IntegrationPluginTempo::onReceivedAccounts(const QList &accounts) +{ + qCDebug(dcTempo()) << "Received" << accounts.count() << "accounts"; + + Tempo *tempoConnection = static_cast(sender()); + Thing *parentThing = m_tempoConnections.key(tempoConnection); + if (!parentThing) + return; + + ThingDescriptors desciptors; + Q_FOREACH(Tempo::Account account, accounts) { + ThingClassId thingClassId; + + Thing * existingThing = myThings().findByParams(ParamList() << Param(m_idParamTypeIds.value(thingClassId), appliance.homeApplianceId)); + if (existingThing) { + qCDebug(dcTempo()) << "Thing is already added to system" << existingThing->name(); + //Set connected state; + //existingThing->setStateValue(m_connectedStateTypeIds.value(thingClassId), appliance.connected); + continue; + } + qCDebug(dcTempo()) << "Found new account:" << account.name << "key:" << account.key << "id:" << account.id; + ThingDescriptor descriptor(thingClassId, account.name, account.key, parentThing->id()); + + ParamList params; + //params << Param(m_idParamTypeIds.value(thingClassId), appliance.homeApplianceId); + descriptor.setParams(params); + desciptors.append(descriptor); + } + if (!desciptors.isEmpty()) + emit autoThingsAppeared(desciptors); +} + diff --git a/tempo/integrationplugintempo.h b/tempo/integrationplugintempo.h new file mode 100644 index 00000000..5ad3bd88 --- /dev/null +++ b/tempo/integrationplugintempo.h @@ -0,0 +1,68 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINTEMPO_H +#define INTEGRATIONPLUGINTEMPO_H + +#include "integrations/integrationplugin.h" +#include "plugintimer.h" + +#include "tempo.h" + +class IntegrationPluginTempo : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationplugintempo.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginTempo(); + + void startPairing(ThingPairingInfo *info) override; + void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override; + + void setupThing(ThingSetupInfo *info) override; + void executeAction(ThingActionInfo *info) override; + void thingRemoved(Thing *thing) override; + +private: + PluginTimer *m_pluginTimer15min = nullptr; + + QHash m_setupTempoConnections; + QHash m_tempoConnections; + +private slots: + void onConnectionChanged(bool connected); + void onAuthenticationStatusChanged(bool authenticated); + void onRequestExecuted(QUuid requestId, bool success); + void onReceivedAccounts(const QList &accounts); +}; +#endif // INTEGRATIONPLUGINTEMPO_H diff --git a/tempo/integrationplugintempo.json b/tempo/integrationplugintempo.json new file mode 100644 index 00000000..1aaede7d --- /dev/null +++ b/tempo/integrationplugintempo.json @@ -0,0 +1,172 @@ +{ + "id": "809bc4ca-d1cd-4279-9e0d-7324537ccb5a", + "name": "tempo", + "displayName": "Tempo", + "apiKeys": ["tempo"], + "paramTypes": [ + { + "id": "c130b2b7-6d30-406e-899b-669a065daee3", + "name": "customClientId", + "displayName": "Custom client id", + "defaultValue": "", + "type": "QString" + }, + { + "id": "9c759711-e772-44ce-9d86-6a3af89c2d94", + "name": "customClientSecret", + "displayName": "Custom client secret", + "defaultValue": "", + "type": "QString" + } + ], + "vendors": [ + { + "id": "58fc1ab7-b8b5-4e52-8388-72957ce5852d", + "name": "tempo", + "displayName": "Tempo", + "thingClasses": [ + { + "id": "878eae0a-6217-4b36-bd46-72c911e52e73", + "name": "tempoConnection", + "displayName": "Tempo connection", + "interfaces": ["account"], + "createMethods": ["user"], + "setupMethod": "oauth", + "paramTypes": [ + { + "id": "b4110c37-8331-4057-8e9f-12f34c2623fe", + "name": "atlassianAccountName", + "displayName": "Atlassian account name", + "type": "QString", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "15f45315-5419-4e1b-ace3-fc21503d3b70", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": true, + "cached": false, + "type": "bool" + }, + { + "id": "e4b5be87-dbc9-481e-88da-608c71be8bda", + "name": "loggedIn", + "displayName": "Logged in", + "displayNameEvent": "Logged in changed", + "defaultValue": true, + "type": "bool" + }, + { + "id": "f3b9581b-7828-4fbe-be5f-3e8aad78a71e", + "name": "userDisplayName", + "displayName": "User name", + "displayNameEvent": "User name changed", + "defaultValue": "", + "type": "QString" + }, + { + "id": "db70444d-bf67-4133-b2de-54aefbdd7149", + "name": "autoAddAccounts", + "displayName": "Auto add accounts", + "displayNameEvent": "Auto add accounts", + "defaultValue": true, + "type": "bool" + } + ] + }, + { + "id": "8be71352-bdfd-450b-903e-79a4ed203701", + "name": "account", + "displayName": "Account", + "interfaces": ["connectable"], + "createMethods": ["auto"], + "browsable": true, + "paramTypes": [ + { + "id": "c6aeddae-56af-496d-a419-1635ff9bae50", + "name": "id", + "displayName": "ID", + "defaultValue": "-", + "type": "QString" + } + ], + "stateTypes": [ + { + "id": "0b776bc1-9e56-4205-9bc3-b356026f5b64", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "defaultValue": true, + "cached": false, + "type": "bool" + }, + { + "id": "7948f15b-7243-404e-9e67-18e915e8b328", + "name": "status", + "displayName": "Status", + "displayNameEvent": "Status changed", + "defaultValue": "OPEN", + "possibleValues": [ + "OPEN", + "CLOSED", + "ARCHIVED" + ], + "type": "QString" + }, + { + "id": "abd55ea0-ad4e-413e-bc77-3e8b7f0a9be4", + "name": "global", + "displayName": "Global", + "displayNameEvent": "Global changed", + "defaultValue": false, + "type": "bool" + }, + { + "id": "44ebbc18-7511-48c0-860b-c4de5f634ed6", + "name": "monthlyBudget", + "displayName": "Monthly budget", + "displayNameEvent": "Monthly budget changed", + "defaultValue": 0, + "type": "int" + }, + { + "id": "f1f2af66-d09a-4242-9058-401145f662c4", + "name": "lead", + "displayName": "Lead", + "displayNameEvent": "Lead changed", + "defaultValue": "", + "type": "QString" + }, + { + "id": "ece43b12-4a0d-4e25-b811-b1aca610bea8", + "name": "contact", + "displayName": "Contact", + "displayNameEvent": "Contact changed", + "defaultValue": "", + "type": "QString" + }, + { + "id": "3af6d1c0-bb0a-406f-809b-2c367e1a16bb", + "name": "category", + "displayName": "Category", + "displayNameEvent": "Category changed", + "defaultValue": "", + "type": "QString" + }, + { + "id": "3dcc1426-51f8-46fa-9967-5a93d7bb2633", + "name": "Customer", + "displayName": "Customer", + "displayNameEvent": "Customer changed", + "defaultValue": "", + "type": "QString" + } + ] + } + ] + } + ] +} diff --git a/tempo/meta.json b/tempo/meta.json new file mode 100644 index 00000000..e69de29b diff --git a/tempo/tempo.cpp b/tempo/tempo.cpp new file mode 100644 index 00000000..fc497ce9 --- /dev/null +++ b/tempo/tempo.cpp @@ -0,0 +1,155 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include +#include + +#include "tempo.h" +#include "extern-plugininfo.h" + +Tempo::Tempo(NetworkAccessManager *networkmanager, const QByteArray &clientId, const QByteArray &clientSecret, QObject *parent) : +QObject(parent), + m_clientId(clientId), + m_clientSecret(clientSecret), + m_networkManager(networkmanager) + +{ + m_tokenRefreshTimer = new QTimer(this); + m_tokenRefreshTimer->setSingleShot(true); + connect(m_tokenRefreshTimer, &QTimer::timeout, this, &Tempo::onRefreshTimer); +} + +QByteArray Tempo::accessToken() +{ + return m_accessToken; +} + +QByteArray Tempo::refreshToken() +{ + return m_refreshToken; +} + +QUrl Tempo::getLoginUrl(const QUrl &redirectUrl, const QString &jiraCloudInstanceName, const QByteArray &clientId) +{ + QUrl url; + url.setScheme("https"); + url.setHost(jiraCloudInstanceName+"atlassian.net"); + url.setPath("/plugins/servlet/ac/io.tempo.jira/oauth-authorize/"); + QUrlQuery query; + query.addQueryItem("client_id", clientId); + query.addQueryItem("redirect_uri", redirectUrl.toString()); + query.addQueryItem("access_type", "tenant_user"); + url.setQuery(query); + return url; +} + +void Tempo::getAccounts() +{ + QUrl url = QUrl(m_baseControlUrl+"/accounts"); + + QNetworkRequest request(url); + request.setRawHeader("Authorization", "Bearer "+m_accessToken); + + QNetworkReply *reply = m_networkManager->get(request); + connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater); + connect(reply, &QNetworkReply::finished, this, [this, reply]{ + + QByteArray rawData = reply->readAll(); + if (!checkStatusCode(reply, rawData)) { + return; + } + QVariantMap dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap(); + QVariantList accountList = dataMap.value("results").toList(); + QList accounts; + Q_FOREACH(QVariant var, accountList) { + QVariantMap map = var.toMap(); + Account account; + account.self = map["self"].toString(); + account.key = map["key"].toString(); + account.id = map["id"].toInt(); + account.name = map["name"].toString(); + + if (map["status"] == "OPEN") { + account.status = Status::Open; + } else if (map["status"] == "CLOSED") { + account.status = Status::Closed; + } else if (map["status"] == "ARCHIVED") { + account.status = Status::Archived; + } + account.global = map["global"].toBool(); + account.monthlyBudget = map["monthlyBudget"].toInt(); + + QVariantMap lead = map["lead"].toMap(); + account.lead.self = lead["self"].toString(); + account.lead.accountId = lead["accountId"].toString(); + account.lead.displayName = lead["displayName"].toString(); + //TODO Customer + QVariantMap customer = map["customer"].toMap(); + account.customer.self = lead["self"].toString(); + //TODO Category + QVariantMap category = map["category"].toMap(); + account.category.self = lead["self"].toString(); + //TODO Contact + QVariantMap contact = map["contact"].toMap(); + account.contact.self = lead["self"].toString(); + + accounts.append(account); + } + if (!accounts.isEmpty()) { + + } + }); +} + +void Tempo::getWorkloadByAccount(const QString &accountKey, QDate from, QDate to) +{ + +} + +void Tempo::onRefreshTimer() +{ + +} + +void Tempo::setAuthenticated(bool state) +{ + if (state != m_authenticated) { + m_authenticated = state; + emit authenticationStatusChanged(state); + } +} + +void Tempo::setConnected(bool state) +{ + if (state != m_connected) { + m_connected = state; + emit connectionChanged(state); + } +} diff --git a/tempo/tempo.h b/tempo/tempo.h new file mode 100644 index 00000000..c6316c1f --- /dev/null +++ b/tempo/tempo.h @@ -0,0 +1,135 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef TEMPO_H +#define TEMPO_H + +#include +#include +#include + +#include "network/networkaccessmanager.h" + +class Tempo : public QObject +{ + Q_OBJECT +public: + + enum Status { + Open, + Closed, + Archived + }; + Q_ENUM(Status) + struct Lead { + QUrl self; + QString accountId; + QString displayName; + }; + + struct Contact { + QUrl self; + QString accountId; + QString displayName; + QString type; + }; + + struct Category { + QUrl self; + QString accountId; + QString displayName; + }; + + struct Customer { + QUrl self; + QString key; + int id; + QString name; + }; + + struct Account { + QUrl self; + QString key; + int id; + QString name; + Status status; + bool global; + int monthlyBudget; + Lead lead; + Contact contact; + Category category; + Customer customer; + }; + + explicit Tempo(NetworkAccessManager *networkmanager, const QByteArray &clientId, const QByteArray &clientSecret, QObject *parent = nullptr); + QByteArray accessToken(); + QByteArray refreshToken(); + + static QUrl getLoginUrl(const QUrl &redirectUrl, const QString &jiraCloudInstanceName, const QByteArray &clientId); + + void getAccessTokenFromRefreshToken(const QByteArray &refreshToken); + void getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode); + + void getAccounts(); + void getWorkloadByAccount(const QString &accountKey, QDate from, QDate to); + +private: + + QByteArray m_baseTokenUrl = "https://api.tempo.io/oauth/token/"; + QByteArray m_baseControlUrl = "https://api.tempo.io/core/3/"; + QByteArray m_clientId; + QByteArray m_clientSecret; + + QByteArray m_accessToken; + QByteArray m_refreshToken; + QByteArray m_redirectUri = "https://127.0.0.1:8888"; + + NetworkAccessManager *m_networkManager = nullptr; + QTimer *m_tokenRefreshTimer = nullptr; + + void setAuthenticated(bool state); + void setConnected(bool state); + + bool m_authenticated = false; + bool m_connected = false; + + bool checkStatusCode(QNetworkReply *reply, const QByteArray &rawData); +private slots: + void onRefreshTimer(); + +signals: + void authenticationStatusChanged(bool state); + void connectionChanged(bool connected); + void receivedRefreshToken(const QByteArray &refreshToken); + void receivedAccessToken(const QByteArray &accessToken); + void accountsReceived(const QList accounts); +}; + +#endif // TEMPO_H diff --git a/tempo/tempo.png b/tempo/tempo.png new file mode 100644 index 0000000000000000000000000000000000000000..28852c46470f550e03691369f7d403e3813cf2d4 GIT binary patch literal 29508 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�YCo6s7k)bF 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 W